Compare commits

..

375 Commits

Author SHA1 Message Date
3d51bcba95 Move release notes to github 2022-01-28 09:27:47 +11:00
3331159821 Merge pull request #719 from sshuttle/revert-713-dependabot/pip/sphinx-4.4.0
Revert "Bump sphinx from 4.3.2 to 4.4.0"
2022-01-22 09:46:01 +11:00
d23a0fd2c5 Revert "Bump sphinx from 4.3.2 to 4.4.0" 2022-01-22 09:44:59 +11:00
164ceac198 Merge pull request #713 from sshuttle/dependabot/pip/sphinx-4.4.0
Bump sphinx from 4.3.2 to 4.4.0
2022-01-22 09:13:26 +11:00
ecc2d68a06 Bump sphinx from 4.3.2 to 4.4.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.2 to 4.4.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.2...v4.4.0)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-20 20:09:06 +00:00
f1bae5ea04 Merge pull request #718 from sshuttle/dependabot/pip/setuptools-scm-6.4.2
Bump setuptools-scm from 6.4.1 to 6.4.2
2022-01-21 07:08:14 +11:00
be667c7854 Bump setuptools-scm from 6.4.1 to 6.4.2
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.4.1 to 6.4.2.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v6.4.1...v6.4.2)

---
updated-dependencies:
- dependency-name: setuptools-scm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-20 10:08:39 +00:00
952336f97b Merge pull request #717 from sshuttle/dependabot/pip/setuptools-scm-6.4.1
Bump setuptools-scm from 6.4.0 to 6.4.1
2022-01-20 07:53:39 +11:00
0890ebd383 Bump setuptools-scm from 6.4.0 to 6.4.1
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v6.4.0...v6.4.1)

---
updated-dependencies:
- dependency-name: setuptools-scm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-19 10:12:35 +00:00
d593e8c4f7 Merge pull request #714 from sshuttle/dependabot/pip/setuptools-scm-6.4.0
Bump setuptools-scm from 6.3.2 to 6.4.0
2022-01-19 07:45:36 +11:00
9429f387ea Bump setuptools-scm from 6.3.2 to 6.4.0
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.3.2 to 6.4.0.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v6.3.2...v6.4.0)

---
updated-dependencies:
- dependency-name: setuptools-scm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-18 10:12:20 +00:00
4e43af758d Merge pull request #708 from skuhl/doas
Allow use of sudo or doas.
2022-01-17 08:04:50 +11:00
0ccd243a65 Merge pull request #712 from skuhl/sudo-use-pty-fix
Fix sshuttle when using sudo's use_pty option.
2022-01-10 10:03:55 +11:00
9e4822d7b7 Merge pull request #711 from skuhl/wait-for-dns-flush
Fix defunct process after flushing DNS cache.
2022-01-09 20:14:02 +11:00
80a822e079 Fix flake8 and unit test errors introduced by use_pty fixes. 2022-01-07 13:21:16 -05:00
8e826cfa7d Print to console with \r\n line endings.
If we run sudo with the use_pty option, the firewall process is
started in a new pseudoterminal. Other processes that are still
printing to the terminal (i.e., the main sshuttle client process,
messages from the shuttle server) have their output incorreclty
displayed. A newline character simply moves the output to the next
line without returning the cursor to the beginning of the line. Simply
changing all print commands to use \r\n line endings fixes the problem
and does not appear to cause any trouble in other configurations.
2022-01-07 13:13:37 -05:00
286bd3fa80 Make setsid() call in firewall process optional.
We previously called setsid() to ensure that the SIGINT generated by
Ctrl+C went to the main sshuttle process instead of the firewall
process. With the previous commit, we gracefully shutdown if either
the sshuttle process or firewall process receives a SIGINT. Therefore,
the setsid() call is optional. We still try calling setsid() since the
preferred shutdown process involves having the signal go to the main
sshuttle process. However, setsid() will fail if the firewall process
is started with sudo and sudo is configured with the use_pty option.
2022-01-07 12:14:57 -05:00
ae8af71886 Gracefully exit if firewall process receives Ctrl+C/SIGINT.
Typically sshuttle exits by having the main sshuttle client process
terminated. This closes file descriptors which the firewall process
then sees and uses as a cue to cleanup the firewall rules. The
firewall process ignored SIGINT/SIGTERM signals and used setsid() to
prevent Ctrl+C from sending signals to the firewall process.

This patch makes the firewall process accept SIGINT/SIGTERM signals
and then in turn sends a SIGINT signal to the main sshuttle client
process which then triggers a regular shutdown as described above.
This allows a user to manually send a SIGINT/SIGTERM to either
sshuttle process and have it exit gracefully. It also is needed if
setsid() fails (known to occur if sudo's use_pty option is used) and
then the Ctrl+C SIGINT signal goes to the firewall process.

The PID of the sshuttle client process is sent to the firewall
process. Using os.getppid() in the firewall process doesn't correctly
return the sshuttle client PID.
2022-01-07 11:52:39 -05:00
54b80e6ce2 Fix defunct process after flushing DNS cache.
When we flush the DNS cache by calling resolvectl, we should wait for
the process to finish. This ensures that the cache is actually flushed
and prevents the process from showing up as defunct when processes are
listed.
2022-01-07 10:45:17 -05:00
b00f2e0a68 Merge pull request #710 from skuhl/tproxy-check-root
Improve error message if tproxy method is used without running as root.
2022-01-06 10:26:27 +11:00
15a8752cc6 Improve error message if tproxy method is used without running as root.
When the tproxy method is used, sshuttle must be run as root:
https://sshuttle.readthedocs.io/en/stable/tproxy.html

Prior to this patch, sshuttle would encounter a exception and print a
message about how a setsockopt() call had a "PermissionError: [Errno 1]
Operation not permitted."

With this patch, we catch this exception, print a more understandable
error message, and exit.

The lack of error message clarity caused at least one bug report: #136
2022-01-04 14:52:21 -05:00
a2776cbbd9 Upgrade Sphinx version 2022-01-04 11:47:35 +11:00
44b772d049 Add readthedocs config 2022-01-04 11:38:07 +11:00
ae1faa7fa1 Merge pull request #709 from skuhl/tproxy-doc-fix
Minor improvement to tproxy documentation.
2022-01-02 12:02:33 +11:00
175da40db7 Fix typo in tproxy documentation.
"IPv6 DNS" was listed twice.
2021-12-31 14:54:17 -05:00
e11db3980f Minor improvement to tproxy documentation.
Previously, tproxy was unique in its support of IPv6. Now, many
sshuttle methods support IPv6 and tproxy remains the only option that
supports UDP.
2021-12-31 14:31:54 -05:00
d6f75fae25 Allow use of sudo or doas.
This is an alternative solution to pull request #611.

Previously, sshuttle would use doas on OpenBSD and sudo on Linux.
However, some Linux distributions are opting to use doas.

This patch changes the logic so that there can be multiple attempts to
elevate privilages. If the first command fails to run, it moves on to
the next command. Part of the existing code looked like it might be
attempting to do this, but it didn't work.

It also looks for the presence of doas and sudo in the path. If we can
find doas (but cannot find sudo) or if the platform is OpenBSD, we try
doas first. Otherwise, we try sudo, then doas. We try all the options
until one succeeds (including running the command without sudo or
doas) regardless of what is in the path. I'm open to adjusting
the logic here based on feedback.

If systems have both sudo and doas, they might be configured to give
different users different permissions. For example, if a user wishes
to use doas on this system, sshuttle would try sudo first and the user
would need to enter invalid passwords to eventually cause sudo to fail
and cause sshuttle to then try doas. This might not be ideal, but it
avoids implement another sshuttle argument that the user would need to
specify. Perhaps machines actually using doas will not have sudo
installed?
2021-12-31 14:20:50 -05:00
354cbe6071 Merge pull request #705 from JohnHay/ipfw
Make ipfw method work
2021-12-24 10:10:02 +11:00
922d827948 Flake8 does not like if we go over 79 characters on a line. 2021-12-22 06:04:25 +02:00
e6f076e1a5 Make flake8 happy. 2021-12-21 16:12:11 +02:00
2665b67926 Remove the ttl hack to allow the host and server to run on the same machine
from the ipfw method.
2021-12-20 11:52:09 +02:00
0cfee0ba84 ipfw expects text. 2021-12-20 09:20:32 +02:00
f247853f39 Changed one place where the Exception text was still tproxy to ipfw. 2021-12-20 08:58:25 +02:00
7c1f2b08cf Indent the else to the same level as its if. 2021-12-20 08:55:38 +02:00
f65a51bbee Add fport and lport (unused) to the for that parse subnets. Otherwise python
is unhappy with "ValueError: too many values to unpack (expected 4)"
2021-12-20 08:53:34 +02:00
b1ee4f1d65 check-state does not take extra options. 2021-12-20 08:37:15 +02:00
4f20efd592 Merge pull request #702 from sshuttle/dependabot/github_actions/actions/setup-python-2.3.1
Bump actions/setup-python from 2.3.0 to 2.3.1
2021-12-01 08:44:47 +11:00
fa0de2d1b7 Bump actions/setup-python from 2.3.0 to 2.3.1
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-30 10:07:48 +00:00
01415dc52e Merge pull request #700 from sshuttle/dependabot/github_actions/actions/setup-python-2.3.0
Bump actions/setup-python from 2.2.2 to 2.3.0
2021-11-19 09:31:07 +11:00
9257077b4e Bump actions/setup-python from 2.2.2 to 2.3.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.2.2 to 2.3.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2.2.2...v2.3.0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-18 10:09:21 +00:00
b896a4b7f3 Remove references to legacy PyXAPI socket_ext for recvmsg
socket.recvmsg has been in Python since version 3.3 and we don't
support anything older then 3.6 the server side.
2021-11-11 08:01:15 +11:00
fca9bd6478 Merge pull request #695 from sshuttle/dependabot/github_actions/actions/checkout-2.4.0
Bump actions/checkout from 2.3.5 to 2.4.0
2021-11-04 07:57:40 +11:00
42bb67b050 Bump actions/checkout from 2.3.5 to 2.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.5...v2.4.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 10:08:19 +00:00
b7800dadda Merge pull request #694 from cclauss/patch-1
pythonpackage.yml: Add Python 3.10 to the testing
2021-11-02 16:59:58 +11:00
3de631abec pythonpackage.yml: Add Python 3.10 to the testing 2021-11-01 19:35:57 +01:00
35382a98a7 Merge pull request #689 from sshuttle/dependabot/github_actions/actions/checkout-2.3.5
Bump actions/checkout from 2.3.4 to 2.3.5
2021-10-19 07:50:09 +11:00
bebfa45626 Bump actions/checkout from 2.3.4 to 2.3.5
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.4...v2.3.5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 10:11:32 +00:00
387dd4775b Merge pull request #687 from sshuttle/dependabot/pip/flake8-4.0.1
Bump flake8 from 4.0.0 to 4.0.1
2021-10-12 07:40:16 +11:00
f5cf79893a Bump flake8 from 4.0.0 to 4.0.1
Bumps [flake8](https://github.com/pycqa/flake8) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/pycqa/flake8/releases)
- [Commits](https://github.com/pycqa/flake8/compare/4.0.0...4.0.1)

---
updated-dependencies:
- dependency-name: flake8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-11 20:38:33 +00:00
86a651099e Bump pyflakes from 2.3.1 to 2.4.0
Bumps [pyflakes](https://github.com/PyCQA/pyflakes) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/PyCQA/pyflakes/releases)
- [Changelog](https://github.com/PyCQA/pyflakes/blob/master/NEWS.rst)
- [Commits](https://github.com/PyCQA/pyflakes/compare/2.3.1...2.4.0)

---
updated-dependencies:
- dependency-name: pyflakes
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-12 07:36:53 +11:00
a42f6a5e67 Bump flake8 from 3.9.2 to 4.0.0
Bumps [flake8](https://github.com/pycqa/flake8) from 3.9.2 to 4.0.0.
- [Release notes](https://github.com/pycqa/flake8/releases)
- [Commits](https://github.com/pycqa/flake8/compare/3.9.2...4.0.0)

---
updated-dependencies:
- dependency-name: flake8
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-12 07:36:18 +11:00
5c4c943db8 Merge pull request #683 from sshuttle/dependabot/pip/pytest-cov-3.0.0
Bump pytest-cov from 2.12.1 to 3.0.0
2021-10-05 08:00:42 +11:00
ccf1c9f478 Bump pytest-cov from 2.12.1 to 3.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.12.1 to 3.0.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.12.1...v3.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-04 10:12:27 +00:00
9399cf10f6 Merge pull request #682 from a1346054/fixes
Simple maintenance improvements
2021-09-23 08:49:18 +10:00
b95c1f3357 Trim excess whitespace 2021-09-22 12:36:36 +00:00
5257d9fd1b Fix shellcheck warnings 2021-09-22 12:36:27 +00:00
ef244a5490 Fix typos 2021-09-22 12:36:26 +00:00
7fb0f0a81b Merge pull request #678 from sshuttle/dependabot/pip/setuptools-scm-6.3.2
Bump setuptools-scm from 6.3.1 to 6.3.2
2021-09-14 07:45:18 +10:00
e8653d444d Bump setuptools-scm from 6.3.1 to 6.3.2
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.3.1 to 6.3.2.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v6.3.1...v6.3.2)

---
updated-dependencies:
- dependency-name: setuptools-scm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-13 10:11:54 +00:00
83038f0472 Merge pull request #676 from balping/master
add openSUSE install instructions
2021-09-07 08:57:25 +10:00
9974ca17f8 add openSUSE install instructions 2021-09-06 23:47:12 +01:00
e2f2fecc8a Merge pull request #675 from sshuttle/dependabot/pip/setuptools-scm-6.3.1
Bump setuptools-scm from 6.1.1 to 6.3.1
2021-09-07 08:26:20 +10:00
b59ce9c014 Bump setuptools-scm from 6.1.1 to 6.3.1
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.1.1 to 6.3.1.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v6.1.1...v6.3.1)

---
updated-dependencies:
- dependency-name: setuptools-scm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-06 10:10:44 +00:00
aa50f762cc Merge pull request #674 from sshuttle/dependabot/pip/pytest-6.2.5
Bump pytest from 6.2.4 to 6.2.5
2021-09-01 08:49:21 +10:00
57640ea2d0 Bump pytest from 6.2.4 to 6.2.5
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.4 to 6.2.5.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.4...6.2.5)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-31 10:09:42 +00:00
e59cc3959c Merge pull request #672 from sshuttle/dependabot/pip/setuptools-scm-6.1.1
Bump setuptools-scm from 6.0.1 to 6.1.1
2021-08-28 17:28:18 +10:00
86d20da82a Bump setuptools-scm from 6.0.1 to 6.1.1
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.0.1 to 6.1.1.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v6.0.1...v6.1.1)

---
updated-dependencies:
- dependency-name: setuptools-scm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-27 10:11:01 +00:00
d3fae535cd Merge pull request #668 from skuhl/use-pty-warning
Improve error message when sudo's use_pty option is enabled.
2021-07-30 07:54:57 +10:00
7d44e890eb Merge pull request #667 from skuhl/server-version-print-early
Print server's python version earlier
2021-07-30 07:52:28 +10:00
68a7766d24 Improve error message when sudo's use_pty option is enabled.
If sudo's use_pty is enabled, the setsid() call in firewall.py will
fail (see #664). When I ignore the error, sshuttle does not behave
properly. This patch explains to the user what is happening and
suggests a workaround.

It might be possible to make sshuttle work with use_pty in the future.
2021-07-29 11:49:09 -04:00
09aab1a0fa Print server's python version earlier
Previously, we printed the server's python version in server.py.
Moving it to assembler.py means that it can be printed earlier so that
it can be seen before a potential error that could occur during
assembly. Additionally, the path to the python executable used by the
server was added to the output as well.

An example of where this would be useful: #666
2021-07-29 11:39:37 -04:00
bd00a530dd Merge pull request #661 from skuhl/no-ttl
Remove ttl hack & require -r option.
2021-07-15 11:47:57 +10:00
bc065e368d Remove ttl hack & require -r option.
Previously, it was possible to run sshuttle locally without using ssh
and connecting to a remote server. In this configuration, traffic was
redirected to the sshuttle server running on the localhost. However,
the firewall needed to distinguish between traffic leaving the
sshuttle server and traffic that originated from the machine that
still needed to be routed through the sshuttle server. The TTL of the
packets leaving the sshuttle server were manipulated to indicate to
the firewall what should happen. The TTL was adjusted for all packets
leaving the sshuttle server (even if it wasn't necessary because the
server and client were running on different machines).

Changing the TTL caused trouble and some machines, and
the --ttl option was added as a workaround to change how the TTL was
set for traffic leaving sshuttle. All of this added complexity to the
code for a feature (running the server on localhost) that is likely
only used for testing and rarely used by others.

This commit updates the associated documentation, but doesn't fully
fix the ipfw method since I am unable to test that.

This change will also make sshuttle fail to work if -r is used to
specify a localhost. Pull request #610 partially addresses that issue.

For example, see: #240, #490, #660, #606.
2021-07-12 11:24:29 -04:00
6ae0b51c61 Merge pull request #658 from skuhl/pfctl-error-report
Print pfctl error message when it returns non-zero.
2021-07-01 08:03:00 +10:00
d7e257642e Print pfctl error message when it returns non-zero.
If pfctl returns non-zero when setting up the firewall, sshuttle exits
and indicates the exit status code. This patch makes it so the output
of pfctl is also printed so the user can get a better idea of what
caused the problem.

For example: issue #491
2021-06-30 14:13:13 -04:00
bea5e1a1f4 Merge pull request #657 from kylekyle/master
Remove psutil from requirements.txt
2021-06-29 08:21:04 +10:00
99787c1459 Remove psutil from requirements.txt 2021-06-26 13:11:24 -04:00
ee02e1aaa2 Merge pull request #656 from kylekyle/master
replace psutil with os
2021-06-25 15:06:39 +10:00
c06581cf83 replace psutil with os 2021-06-23 21:53:03 -04:00
78b80709f2 Merge pull request #650 from skuhl/hw-improve
Improve hostwatch robustness and documentation
2021-06-03 07:28:12 +10:00
d3f4889f21 fix lint errors 2021-06-02 15:32:04 -04:00
560c6b4ce8 Improve hostwatch robustness and documentation.
If an exception occurs in hostwatch, sshuttle exits. Problems
read/writing the ~/.sshuttle.hosts cache file on the remote machine
would therefore cause sshuttle to exit. With this patch, we simply
continue running without writing/reading the cache file in the remote
home directory. This serves as an alternate fix for
pull request #322 which proposed storing the cache file elsewhere.

A list of included changes:

- If we can't read or write the host cache file on the server,
  continue running. Hosts can be collected through the netstat,
  /etc/hosts, etc and the information can be reconstructed each run if
  a cache file isn't available to read. We write a log() message when
  this occurs.

- Add additional types of exceptions to handle.

- Continue even if we cannot read /etc/hosts on the server.

- Update man page to mention the cache file on the remote host.

- Indicate that messages are related to remote host instead of local
  host.

- Add comments and descriptions to the code.
2021-06-02 15:22:04 -04:00
d21a322f05 Merge pull request #649 from sshuttle/dependabot/pip/pytest-cov-2.12.1
Bump pytest-cov from 2.12.0 to 2.12.1
2021-06-02 20:20:55 +10:00
3e308b4266 Bump pytest-cov from 2.12.0 to 2.12.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.12.0 to 2.12.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.12.0...v2.12.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-02 06:20:06 +00:00
a3cbf0885f Merge pull request #646 from skuhl/nat-ipv6
Add IPv6 support to nat (iptables) method.
2021-06-01 07:40:08 +10:00
3f201095ea Merge branch 'master' into nat-ipv6 2021-05-30 21:09:32 -04:00
58c264ff1c Merge pull request #648 from skuhl/sudoers-add-sanitize
Fix #637: sudoers-add should always write to /etc/sudoers.d/...
2021-05-31 07:53:19 +10:00
1820264dd5 Merge pull request #647 from skuhl/man-page-updates
Update documentation
2021-05-31 07:48:56 +10:00
be847f5d81 Merge pull request #645 from skuhl/hostwatch-remove-dead-code
Remove dead code in hostwatch.py
2021-05-31 07:44:21 +10:00
d2836a082f Merge pull request #644 from skuhl/fix-tmark
Fix --tmark option
2021-05-31 07:42:30 +10:00
8c5ffc9e72 Fix #637: File written by sudoers-add should always begin with /etc/sudoers.d/
The problem and patch was originally submitted aayla-secura. I made a
minor improvement to the error message.
2021-05-30 17:03:03 -04:00
843a729d64 flake8: fix long line 2021-05-29 21:58:56 -04:00
851b26cb13 Update documentation
The output in the examples provided in the man page hadn't been
updated as sshuttle changed its output over time.

The example of testing sshuttle without a remote host was removed. It
was the first example previously and it is something that is unlikely
users will wish to do.

Also:
 - Update some --help messages.
 - Manpage: Fix a typo.
 - Manpage: Mention that host specified with -r can be an ssh alias.
 - Eliminate variable only used once.
2021-05-29 21:54:47 -04:00
c026a92cad Add IPv6 support to nat (iptables) method.
Adding IPv6 support to the nat method is straightforward after the
previous work to add IPv6 support for nft.
2021-05-29 20:55:48 -04:00
1dbec7252d Remove dead code in hostwatch.py
11 years ago in commit 384d0e7c1d,
hostwatch was updated to use netstat to find hosts, and
_check_smb()/_check_nmb() were edited to immediately return. This
patch removes all of the unused code in these two functions.
2021-05-29 20:48:33 -04:00
a7df12cd68 Fix --tmark option
Even when --tmark was used, the iptables code always used '1' for the
mark. This patch corrects the problem.

Previously, it wasn't clear if the tmark should be supplied in
hexadecimal or as an integer. This makes it use hexadecimal, checks
that the input is hexadecimal, and updates the associated
documentation.

This patch also makes --ttl information get passed to the firewall in
a way that matches how other information gets passed. The ttl and
tmark information are passed next to each other in many places and
this patch also makes the order consistent.
2021-05-27 21:48:43 -04:00
bc54ffe398 Merge pull request #643 from skuhl/fix-to-ns-with-ns-hosts
Fix: Allow --to-ns and --ns-host without --dns.
2021-05-22 13:06:48 +10:00
31f059883c Fix: Allow --to-ns and --ns-host without --dns.
If the user specifies --to-ns (tells the remote server which DNS
server to use for lookups coming from sshuttle), then either --ns-host
or --dns need to also be used (route DNS requests through sshuttle).

A previous commit incorrectly made it so --to-ns and --ns-host
couldn't be successfully used together.

Attempts to fix #641.
2021-05-21 17:47:38 -04:00
f376674941 Merge pull request #634 from skuhl/systemd-dns-flush
Flush systemd DNS cache on startup and exit.
2021-05-18 15:40:56 +10:00
41ca86dbf2 Merge pull request #640 from sshuttle/dependabot/pip/pytest-cov-2.12.0
Bump pytest-cov from 2.11.1 to 2.12.0
2021-05-18 08:17:26 +10:00
7760d63870 Bump pytest-cov from 2.11.1 to 2.12.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.11.1 to 2.12.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.11.1...v2.12.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 07:36:53 +00:00
8b69c706c2 Merge pull request #639 from sshuttle/dependabot/github_actions/actions/setup-python-2.2.2
Bump actions/setup-python from 2 to 2.2.2
2021-05-12 20:36:42 +10:00
d289fb2e73 Merge pull request #638 from sshuttle/dependabot/github_actions/actions/checkout-2.3.4
Bump actions/checkout from 2 to 2.3.4
2021-05-12 20:36:28 +10:00
20c38afaba Bump actions/setup-python from 2 to 2.2.2
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 2.2.2.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v2.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 06:13:47 +00:00
7aeb7929c5 Bump actions/checkout from 2 to 2.3.4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v2.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 06:13:43 +00:00
30767135fb Merge pull request #636 from sshuttle/dependabot/pip/flake8-3.9.2
Bump flake8 from 3.9.1 to 3.9.2
2021-05-10 20:53:51 +10:00
df383edaf6 Bump flake8 from 3.9.1 to 3.9.2
Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.1 to 3.9.2.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.1...3.9.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 10:00:12 +00:00
88154f4f9c Merge pull request #635 from skuhl/sudoers-warning
Warn about adding sshuttle to sudoers.
2021-05-09 20:25:26 +10:00
915497f73f Warn about adding sshuttle to sudoers.
Issue #631 suggests that we should warn about users who add sshuttle
to sudoers because it isn't obvious that when a user can run sshuttle
as root, they can run any command as root using sshuttle's -e or
--ssh-cmd parameters.

This patch adds a comment that warns about this problem to the sudoers
file. It also prints the warning to the console if the user uses an
option that writes the data directly to the file. This patch also
causes the output of the sudoers-add command to be printed to the
console so that the user can see the name of the file that was
created.

There is room for improvement: Warnings could be added to the
documentation and/or these parameters could be removed entirely.
2021-05-07 14:13:56 -04:00
652113818e Merge pull request #633 from sshuttle/dependabot/pip/pytest-6.2.4
Bump pytest from 6.2.3 to 6.2.4
2021-05-06 08:08:28 +10:00
cdfb4b7d71 Flush systemd DNS cache on startup and exit.
It is possible for DNS requests to go through systemd's DNS resolution
system (which includes a cache) before sshuttle has an opportunity to
intercept the requests. The DNS entries in the cache may become
outdated when sshuttle starts or exits. This patch fixes the problem
by flushing the cache when sshuttle firewall starts and exits.
2021-05-05 12:24:58 -04:00
45b788c908 Bump pytest from 6.2.3 to 6.2.4
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.3...6.2.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-05 07:58:09 +00:00
f6176d3581 Merge pull request #630 from thesamesam/patch-1
README.rst: fix Gentoo entry syntax
2021-04-27 07:47:46 +10:00
bc08198cae README.rst: fix Gentoo entry syntax 2021-04-26 16:22:42 +01:00
613412b3cd Merge pull request #627 from sshuttle/dependabot/pip/flake8-3.9.1
Bump flake8 from 3.9.0 to 3.9.1
2021-04-18 10:26:40 +10:00
4f7706db59 Bump flake8 from 3.9.0 to 3.9.1
Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.0 to 3.9.1.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.0...3.9.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-16 08:00:29 +00:00
7307636d35 Merge pull request #624 from sshuttle/dependabot/add-v2-config-file
Create Dependabot config file
2021-04-08 11:47:33 +10:00
1f5161e48c Create Dependabot config file 2021-04-08 01:40:51 +00:00
d505b08104 Merge pull request #623 from sshuttle/dependabot/pip/pytest-6.2.3
Bump pytest from 6.2.2 to 6.2.3
2021-04-06 08:37:03 +10:00
3e3608f572 Bump pytest from 6.2.2 to 6.2.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.2 to 6.2.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.2...6.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-04-05 06:17:47 +00:00
51a1078371 Merge pull request #622 from necrose99/patch-1
Update README.rst
2021-03-26 07:52:21 +11:00
f55983e485 Merge pull request #620 from sshuttle/dependabot/pip/pyflakes-2.3.1
Bump pyflakes from 2.3.0 to 2.3.1
2021-03-26 07:51:48 +11:00
e54747bfb0 Update README.rst
https://packages.gentoo.org/packages/net-proxy/sshuttle
2021-03-25 14:12:32 -05:00
d8acf15c1e Bump pyflakes from 2.3.0 to 2.3.1
Bumps [pyflakes](https://github.com/PyCQA/pyflakes) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/PyCQA/pyflakes/releases)
- [Changelog](https://github.com/PyCQA/pyflakes/blob/master/NEWS.rst)
- [Commits](https://github.com/PyCQA/pyflakes/compare/2.3.0...2.3.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-03-25 06:12:41 +00:00
bafc0a7038 Merge pull request #618 from patrislav1/bugfix-dnsproxy
Fix DnsProxy
2021-03-21 09:21:26 +11:00
4c11dd7183 options: make sure ttl is of integer type 2021-03-20 12:26:17 +01:00
4257c8d34e Merge pull request #617 from patrislav1/bugfix-iptables-wait
Fix firewall setup/teardown failure on xtable lock
2021-03-20 11:27:20 +11:00
dc657a93f4 Merge pull request #616 from sshuttle/dependabot/pip/setuptools-scm-6.0.1
Bump setuptools-scm from 5.0.2 to 6.0.1
2021-03-19 09:01:02 +11:00
5e177d81bc Fix firewall setup/teardown failure on xtable lock 2021-03-18 09:41:21 +01:00
7015d7a823 Bump setuptools-scm from 5.0.2 to 6.0.1
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 5.0.2 to 6.0.1.
- [Release notes](https://github.com/pypa/setuptools_scm/releases)
- [Changelog](https://github.com/pypa/setuptools_scm/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/setuptools_scm/compare/v5.0.2...v6.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-03-18 06:10:30 +00:00
014a268b99 Merge pull request #614 from sshuttle/upgrade_lint
Bump pyflakes from 2.2.0 to 2.3.0, flake8 from 3.8.4 to 3.9.0
2021-03-16 08:30:15 +11:00
97076a9390 Adjust branches, remove temporary branches 2021-03-16 08:29:14 +11:00
dd661e9728 Bump pyflakes from 2.2.0 to 2.3.0, flake8 from 3.8.4 to 3.9.0
Bumps [pyflakes](https://github.com/PyCQA/pyflakes) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/PyCQA/pyflakes/releases)
- [Changelog](https://github.com/PyCQA/pyflakes/blob/master/NEWS.rst)
- [Commits](https://github.com/PyCQA/pyflakes/compare/2.2.0...2.3.0)

Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.4 to 3.9.0.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.4...3.9.0)
2021-03-16 08:21:03 +11:00
d6d11b24c8 Make exit code indicate a problem when pidfile is not writable.
Here, we try to open the pidfile for writing prior to forking so that
the exit code can properly indicate to the user that there was a
problem. No error messages are printed to the console in this case
because when --daemon implies --syslog. So, the syslog will contain
the message indicating that the pidfile wasn't writeable.

Fixes bug #598.
2021-03-10 07:54:55 +11:00
bb1363ec6b Bump setuptools-scm from 5.0.1 to 5.0.2
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 5.0.1 to 5.0.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/v5.0.1...v5.0.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-03-06 11:19:54 +11:00
167a57e739 firewall: Allow overriding the TTL
In instances where a cluster pod in a local VM needs to access a server
that is sshuttle'd from the host, since the packets arriving at the host
already made a hop, their TTL is 63 and so get ignored by sshuttle.
Allowing an override of the firewall TTL rule allows the packets to go
through.
2021-03-05 08:53:53 +11:00
0e51da519f Allow comments in configuration file 2021-02-16 07:51:32 +11:00
a22c453d5e Remove trailing whitespace 2021-02-16 07:51:32 +11:00
f928f94093 Bump pytest from 6.2.1 to 6.2.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.1 to 6.2.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/6.2.1...6.2.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-26 18:22:31 +11:00
127cac37ef --latency-buffer-size now impacts server's buffer.
sshuttle has a --latency-buffer-size parameter, but it only changes
the buffer size on the client and not the server. Therefore,
increasing or decreasing the number doesn't make any change in
download performance (like the documentation indicates that it should).

You can test this change by setting up a sshuttle connection and
downloading a large file through sshuttle. With this patch, you should
find that increasing --latency-buffer-size increases the download
speed. Without the patch, the parameter should have little impact on
performance.
2021-01-23 10:01:42 +11:00
d68f57b534 Bump pytest-cov from 2.11.0 to 2.11.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.11.0 to 2.11.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.11.0...v2.11.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-22 08:45:52 +11:00
0933684d0f Bump pytest-cov from 2.10.1 to 2.11.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.10.1 to 2.11.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.10.1...v2.11.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-19 20:35:20 +11:00
bf7b4a6c34 chore: remove attrs from requirements-test.txt
It used to be necessary to fix the version of attrs when using pytest. This problem has been fixed now, so I removed it.
https://stackoverflow.com/a/58198754
2021-01-19 09:14:03 +11:00
65d437a9f8 feat: remove mock from test dependencies.
Because mock can be replace by unittest.mock
2021-01-17 15:42:55 +11:00
254b8e7987 remove py35 from tox.ini
Because the supported version is python3.6 and above.
2021-01-17 15:42:24 +11:00
97c25e988e tproxy: Skip firewall chains if packets have local destination. (#578)
If you use the tproxy method with a large subnet (such as 0/0), then
(1) you may not receive UDP packets that sshuttle/tproxy can handle
and (2) you are unable to connect to your machine using an IP that
your computer recognizes as its own.

To resolve those issues, any traffic to an IP that the host knows is
local, does not go through the sshuttle chains.
2021-01-04 09:05:32 +11:00
b7730fc106 Improve error messages related to sshuttle server. (#580)
* Improve error messages related to sshuttle server.

There are many GitHub issues related to the cryptic message:
fatal: expected server init string 'SSHUTTLE0001'; got b''

The code that prints that message is after another check that is
intended to verify that the server is still running. This code was
faulty since the server is still running when rv==None (but exited
when rv==0).

I corrected this problem and then investigated ways to clarify the
error message. I added additional exit codes for the server: 97 (exec
in the shell returned), 98 (the python exec() function called
returned). The end result is that the cryptic error message above will
now print a more appropriate error message that should aid in
debugging.

I also changed the server so that it catches Fatal() and exits with
exit code 99 (like the client does). Previously, it was just an
unhandled exception on the server.

I suspect some of the error messages were caused by restricted shells.
I also investigated and added comments about how sshuttle might behave
if it is being run on a server that has a restricted shell.

This commit also replaces a couple of exit() calls in cmdline.py with
'return' since exit() is intended for interactive use. This change
doesn't impact the server.

* Remind user to exclude remote host when server exits with 255.
2021-01-04 08:35:10 +11:00
7fc33c0020 Refactor debug, log and Fatal messages.
This commit rewrites the log() function so that it will append a
newline at the end of the message if none is present. It doesn't make
sense to print a log message without a newline since the next log
message (which will write a prefix) expects to be starting at the
beginning of a line.

Although it isn't strictly necessary, this commit also removes any
newlines at the ends of messages. If I missed any, including the
newline at the end of the message will continue to work as it did
before.

Previously, some calls were missing the newline at the end even though
including it was necessary for subsequent messages to appear
correctly.

This code also cleans up some redundant prefixes. The log() method
will prepend the prefix and the different processes should set their
prefix as soon as they start.

Multiline messages are still supported (although the prefix for the
additional lines was changed to match the length of the prefix used
for the first line).
2021-01-01 19:32:48 +11:00
563f41478a Ignore quotes in config file passed to sshuttle with @
When users put parameters in a config file and pass them to sshuttle
using '@', they might copy the quotes from the command line into the
config file. This fix first ensures that we strip whitespace from the
beginning/end of each line in the config file. Then, if the line
begins and ends with a matching quote character, strip those too.

Fixes #573.
2020-12-31 10:20:22 +11:00
da848b6dba Readme: add instructions for installing via MacPorts 2020-12-29 19:12:06 +11:00
bc72bb4811 Release version 1.0.5 2020-12-29 10:34:58 +11:00
b8cd2fae40 Add Python 3.9 support 2020-12-28 11:01:07 +11:00
8f0d3b0f8e Add release notes for new release 2020-12-28 10:56:01 +11:00
6d4261e3f9 Refactor automatic method selection.
Add an "is_supported()" function to the different methods so that each
method can include whatever logic they wish to indicate if they are
supported on a particular machine. Previously, methods/__init__.py
contained all of the logic for selecting individual methods. Now, it
iterates through a list of possible options and stops on the first
method that it finds that is_supported().

Currently, the decision is made based on the presence of programs in
the PATH. In the future, things such as the platform sshuttle is
running on could be considered.
2020-12-28 10:21:56 +11:00
7c338866bf Set default tmark to pass coverage tests
Signed-off-by: Samuel Bernardo <samuel@lip.pt>
2020-12-28 10:20:46 +11:00
6b87ad3fc7 Set default tmark to pass coverage tests
Signed-off-by: Samuel Bernardo <samuel@lip.pt>
2020-12-28 10:20:46 +11:00
0efd23f3b8 Correct options typo for argument tmark
Signed-off-by: Samuel Bernardo <samuel@lip.pt>
2020-12-28 10:20:46 +11:00
9bdd9fea5d Correct flake8 liting issues
Signed-off-by: Samuel Bernardo <samuel@lip.pt>
2020-12-28 10:20:46 +11:00
d5cceb3e42 Add workflow_dispatch to github actions
Signed-off-by: Samuel Bernardo <samuel@lip.pt>
2020-12-28 10:20:46 +11:00
65b139ff6e Add current branch to github workflow for testing
Signed-off-by: Samuel Bernardo <samuel@lip.pt>
2020-12-28 10:20:46 +11:00
76b8b83e22 Add .gitignore .vscode/ path. Resolve the issue #374 adding tproxy mark option to allow different network mapping.
Signed-off-by: Samuel Bernardo <samuel@lip.pt>
2020-12-28 10:20:46 +11:00
a5214e0fd7 Bump mock from 2.0.0 to 4.0.3
Bumps [mock](https://github.com/testing-cabal/mock) from 2.0.0 to 4.0.3.
- [Release notes](https://github.com/testing-cabal/mock/releases)
- [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/testing-cabal/mock/compare/2.0.0...4.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-16 20:15:42 +11:00
3861d29de2 Merge pull request #571 from sshuttle/dependabot/pip/pytest-6.2.1
Bump pytest from 6.2.0 to 6.2.1
2020-12-16 20:15:16 +11:00
59a983f9a6 Bump pytest from 6.2.0 to 6.2.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.0...6.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-16 09:14:24 +00:00
4a65f97c8b Drop testing of Python 3.5
Due to message from CI:

DEPRECATION: Python 3.5 reached the end of its life on September 13th,
2020. Please upgrade your Python as Python 3.5 is no longer maintained.
pip 21.0 will drop support for Python 3.5 in January 2021. pip 21.0 will
remove support for this functionality.
2020-12-16 20:11:13 +11:00
461e676973 Merge pull request #570 from sshuttle/dependabot/pip/pytest-6.2.0
Bump pytest from 6.1.2 to 6.2.0
2020-12-14 19:12:11 +11:00
70e3e017ab Merge pull request #569 from sshuttle/dependabot/pip/setuptools-scm-5.0.1
Bump setuptools-scm from 4.1.2 to 5.0.1
2020-12-14 19:11:43 +11:00
26704cf742 Bump pytest from 6.1.2 to 6.2.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.2 to 6.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.1.2...6.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-14 06:47:57 +00:00
28a85928be Bump setuptools-scm from 4.1.2 to 5.0.1
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 4.1.2 to 5.0.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.2...v5.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-14 06:47:33 +00:00
ff70f584d2 Merge pull request #561 from Krout0n/fix-handling-OSError
Fix handling OSError in FirewallClient#__init__
2020-11-16 17:00:29 +11:00
5ff834bd32 Fix handling OSError in FirewallClient#__init__ 2020-11-16 10:36:39 +09:00
6b75d62d68 Merge pull request #560 from sshuttle/dependabot/pip/attrs-20.3.0
Bump attrs from 20.2.0 to 20.3.0
2020-11-07 11:19:49 +11:00
6bbe8c0d34 Bump attrs from 20.2.0 to 20.3.0
Bumps [attrs](https://github.com/python-attrs/attrs) from 20.2.0 to 20.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/20.2.0...20.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-06 06:35:04 +00:00
7e726bc235 Merge pull request #558 from skuhl/dns-to-none
Fix "DNS request from ... to None" messages.
2020-11-05 07:30:09 +11:00
227412e218 Fix long line in previous commit 2020-11-04 11:40:07 -05:00
9b036fc689 Merge pull request #552 from skuhl/systemd-resolved
Intercept DNS requests sent by systemd-resolved.
2020-11-04 16:55:09 +11:00
34acdd0611 Merge pull request #557 from skuhl/nft-ipv6-improvements
Improve nft IPv6 support.
2020-11-04 16:52:49 +11:00
8461e08bc3 Make server and client handle resolv.conf differently.
The server should just read from resolv.conf to find DNS servers to
use. This restores this behavior after the previous commit changed it.

The client now reads both /etc/resolv.conf and
/run/systemd/resolve/resolv.conf. The latter is required to more
reliably intercept regular DNS requests that systemd-resolved makes.
2020-11-03 20:27:57 -05:00
d3700f09da Improve nft IPv6 support.
This commit makes two fixes:

1. If an IPv6 DNS server is used, an nft rule had "ip6 protocol" in it
which is invalid and caused sshuttle to exit.

2. I modified detection of udp vs tcp to follow the recommendation at
https://superuser.com/questions/1560376/match-ipv6-protocol-using-nftables

I also re-arranged the code slightly to reduce the number of
if-statements.
2020-11-03 20:14:56 -05:00
92b99442c3 Merge pull request #551 from skuhl/which-fix
Improve consistency of PATH, environments, and which()
2020-11-04 08:00:53 +11:00
709e5d1595 Improve error message when "ip" and "netstat" are missing and --auto-nets fails to work 2020-11-03 12:53:16 -05:00
b5aaeda2a8 Merge pull request #553 from sshuttle/dependabot/pip/pytest-6.1.2
Bump pytest from 6.1.1 to 6.1.2
2020-10-29 18:17:19 +11:00
0ce268f21b Bump pytest from 6.1.1 to 6.1.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.1 to 6.1.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/6.1.1...6.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-29 06:35:08 +00:00
34f538ff98 Merge branch 'master' into which-fix to resolve merge conflict. 2020-10-26 17:24:32 -04:00
7d89b2d89f Fix "DNS request from ... to None" messages.
Some methods are unable to determine the destination address of DNS
packets that we capture. When this happens, change the message so it
just shows where the DNS requests are from.
2020-10-26 15:46:01 -04:00
502960d796 Intercept DNS requests sent by systemd-resolved.
Previously, we would find DNS servers we wish to intercept traffic on
by reading /etc/resolv.conf. On systems using systemd-resolved,
/etc/resolv.conf points to localhost and then systemd-resolved
actually uses the DNS servers listed in
/run/systemd/resolve/resolv.conf. Many programs will route the DNS
traffic through localhost as /etc/resolv.conf indicates and sshuttle
would capture it. However, systemd-resolved also provides other
interfaces for programs to resolve hostnames besides the localhost
server in /etc/resolv.conf.

This patch adds systemd-resolved's servers into the list of DNS
servers when --dns is used.

Note that sshuttle will continue to fail to intercept any traffic sent
to port 853 for DNS over TLS (which systemd-resolved also supports).

For more info, see:
sshuttle issue #535
https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html
https://github.com/systemd/systemd/issues/6076
2020-10-25 12:29:32 -04:00
5c8c707208 Merge pull request #550 from skuhl/nft-ipv6
IPv6 support in nft method.
2020-10-25 15:55:33 +11:00
68c9c9bbcd Improve consistency of PATH, environments, and which()
This patch attempts to fix (or aid in debugging) issue #350.

sshuttle didn't explicitly search /sbin and /usr/sbin and they may be
missing in the user's PATH. If PATH is missing, these folders wouldn't
be searched either. There was also a program_exists function which is
redundant to which(). This consolidates everything into the helpers.py
file.

This patch introduces get_path() to return PATH + some extra hardcoded
paths. A new get_env() function can be called to create a consistent
environment when calling external programs. The new which() wrapper
function also ensures we use the same set of paths.

If -vv is supplied, messages clearly indicate the programs we are
looking for, if they are found, and where we looked if we failed to
find them.

I haven't tested the changes to ipfw or pf.
2020-10-23 20:33:20 -04:00
c02b93e719 nft IPv6 documentation (and other minor doc updates)
Update docs to indicate that IPv6 is supported with the nft method.

- Adds nft into the requirements.rst file.

- Update description of what happens when a hostname is used in a
  subnet.

- Add ipfw to list of methods.

- Indicate that --auto-nets does not work with IPv6. Previously this
  was only mentioned in tproxy.rst

- Clarify that we try to use "python3" on the server before trying
  "python".
2020-10-22 20:17:09 -04:00
6d86e44fb4 IPv6 support in nft method.
This works for me but needs testing by others. Remember to specify a
::0/0 subnet or similar to route IPv6 through sshuttle.

I'm adding this to nft before nat since it is not sshuttle's default
method on Linux. Documentation updates may be required too.

This patch uses the ipaddress module, but that appears to be included
since Python 3.3.
2020-10-21 17:47:07 -04:00
ebf87d8f3b Merge pull request #549 from skuhl/nft-nat-update
Make nat and nft rules consistent; improve rule ordering.
2020-10-22 07:56:37 +11:00
bc24ed359a Make nat and nft rules consistent; improve rule ordering.
First, check if TTL indicates we should ignore packet (instead of
checking in multiple rules later). Also, nft method didn't do this at
all. Now, nft matches the behavior of nat.

Second, forward DNS traffic (we may need to intercept traffic to
localhost if a DNS server is running on localhost).

Third, ignore any local traffic packets. (Previously, we ignored local
traffic except DNS and then had the DNS rules). The nft method didn't
do this previously at all. It now matches the behavior of nat.

Lastly, list the subnets to redirect and/or exclude. This step is left
unchanged. Excluding the local port that we are listening on is
redundant with the third step, but should cause no harm.

In summary, this ordering simplifies the rules in nat and eliminates
differences that previously existed between nat and nft.
2020-10-21 11:51:39 -04:00
ac3ccb769a Merge pull request #544 from skuhl/fix-no-remote
Allow no remote to work.
2020-10-21 07:53:40 +11:00
1f3c74a1af Merge pull request #548 from skuhl/stdout-cleanup
Make prefixes in verbose output more consistent.
2020-10-21 07:53:14 +11:00
512a3a8d96 Add missing space in client ssh error message 2020-10-20 13:38:37 -04:00
4deee45bc6 whitespace cleanup 2020-10-20 13:35:32 -04:00
7cb30b783d Make prefixes in verbose output more consistent.
Use 'c' prefix for client, 's' prefix for server, and 'fw' prefix for
firewall messages. The 'c' and 's' prefixes were used sometimes but
not consistently. The firewall printed messages prefixed with
"firewall manager:" or "firewall:" or ">>" previously.

This patch also fixes a couple of print() calls that should have been
debug1()---a bug introduced in a recent commit.
2020-10-20 13:29:45 -04:00
fb4950fafc Merge pull request #547 from nickray/document-subnets-option-in-man-page
Document -s/--subnets option in man page
2020-10-20 07:57:21 +11:00
c3016f2d90 Merge pull request #541 from skuhl/use-all-ips
When subnets and excludes are specified with hostnames, use all IPs.
2020-10-20 07:56:50 +11:00
9d704b3503 Document -s/--subnets option in man page 2020-10-19 13:35:03 +02:00
a266e7a8bd Merge pull request #545 from skuhl/avoid-touching-etc-hosts
Only write /etc/hosts when necessary.
2020-10-19 15:53:14 +11:00
e1106a33a9 Only write /etc/hosts when necessary.
Without this patch, sshuttle 'restores' /etc/hosts even if it didn't
make any modifications to it. This can be confirmed by running without
--auto-hosts and confirming that the modification time of /etc/hosts
is unchanged while sshuttle is running, but is updated when sshuttle
exits (and a debug2() message is printed indicating the file is
written).

I'm not aware of the previous behavior causing problems. However,
writing an important file unnecessarily as root should be avoided.
2020-10-19 00:17:37 -04:00
574ed8e564 Allow no remote to work.
Pull request #502 made -r/--remote required. However, the
documentation still indicates that using no remote is a valid way to
test sshuttle (see Examples section of man page). I think this mode
might be useful for testing performance local without ssh, local with
ssh, and remote with ssh.

This patch adds a warning when -r/--remote is missing but restores the
previous behavior.
2020-10-18 23:54:18 -04:00
1dbf216369 Merge pull request #543 from skuhl/sdnotify-doc
sdnotify.py documentation
2020-10-19 09:49:16 +11:00
52558174b8 sdnotify.py documentation 2020-10-18 16:45:57 -04:00
b7a29acab7 Update/document client's handling of IPv4 and IPv6.
Additional comments, checks, warning messages, and diagnostic
information is printed out when the client starts.

We assume IPv4 is always present and enabled. We assume IPv6 is not
supported when it is disabled at the command line or when it is not
supported by the firewall method. Warn if IPv6 is disabled but the
user specified IPv6 subnets, IPv6 DNS servers, or IPv6 excludes that
are effectively ignored.

Instead of indicating which features are on/off, we also indicate if
features are available in the verbose output.

We also more clearly print the subnets that we forward, excludes, and
any redirected DNS servers to the terminal output.

These changes should help handling bug reports and make it clearer to
users what is happening. It should also make it more graceful when a
user specifies a subnet/exclude with hostname that resolves to both
IPv4 and IPv6 (but IPv6 is disabled in sshuttle).
2020-10-18 16:30:29 -04:00
c2b10465e7 Remove localhost test since it can resolve to either IPv4, IPv6, or both in any particular order 2020-10-17 15:56:23 -04:00
cfe14f2498 fix flake8 issues in updated tests 2020-10-17 15:40:28 -04:00
cb53d8a150 Make tests for parse_subnetport() expect lists & update expected error messages in tests 2020-10-17 15:36:16 -04:00
64d5c77a71 fix flake8 issues, clarify comment 2020-10-17 14:43:09 -04:00
036c49e412 When subnets and excludes are specified with hostnames, use all IPs.
The list of subnets to route over VPN and the list of subnets to
exclude are parsed in option.py parse_subnetport(). Hostnames or IP
addresses are supported. If a hostname was provided, only the first IP
address was considered. This could result in some traffic not
traversing the VPN that the user might expect should traverse it from
the arguments passed to sshuttle.

This patch makes the function handle all of the IPs if a hostname is
provided. If a user provides a hostname with a CIDR mask, problems can
occur and we warn the user about the issue.

If the user includes a hostname with both an IPv4 and an IPv6 address,
and the underlying method doesn't support IPv6, then this patch will
cause sshuttle to fail. I plan to provide a future patch where failure
won't occur if the only place IPv6 addresses appear is in the exclude
list. In that case it should be safe to ignore the IPv6 address.

This patch also changes parse_ipport() which is used by the --to-ns
option. If the user provides a hostname here, we just use the first IP
from the hostname and warn the user that only one is being used.
2020-10-16 18:29:16 -04:00
c1cc3911df Merge pull request #537 from skuhl/add-version
Include sshuttle version in verbose output.
2020-10-10 11:18:13 +11:00
84e43d3113 Include sshuttle version in verbose output.
Some bug reports include verbose sshuttle output but lack the version
that is being used. Including the sshuttle version in the output may
make it easier to handle future bug reports.
2020-10-08 22:39:42 -04:00
afad317f2c Merge pull request #536 from ed-velez/add_psutil_to_setup
Add psutil as dependency in setup.py
2020-10-08 08:09:21 +11:00
ae5dbd3b4d Add psutil as dependency in setup.py 2020-10-07 15:00:45 -05:00
2995a624f1 Merge pull request #534 from sshuttle/dependabot/pip/flake8-3.8.4
Bump flake8 from 3.8.3 to 3.8.4
2020-10-06 07:56:56 +11:00
909402a353 Merge pull request #533 from sshuttle/dependabot/pip/pytest-6.1.1
Bump pytest from 6.1.0 to 6.1.1
2020-10-06 07:56:36 +11:00
16148ac70f Bump flake8 from 3.8.3 to 3.8.4
Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.3 to 3.8.4.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.3...3.8.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-05 06:37:36 +00:00
e50bbc2c92 Bump pytest from 6.1.0 to 6.1.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.0 to 6.1.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.1.0...6.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-05 06:37:12 +00:00
9eaed73098 Merge pull request #529 from sshuttle/dependabot/pip/pytest-6.1.0
Bump pytest from 6.0.2 to 6.1.0
2020-09-29 07:40:40 +10:00
4b07dab9dc Bump pytest from 6.0.2 to 6.1.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.2 to 6.1.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.0.2...6.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-28 06:43:22 +00:00
299854d2b5 Merge pull request #522 from sshuttle/dependabot/pip/pytest-6.0.2
Bump pytest from 6.0.1 to 6.0.2
2020-09-15 07:33:28 +10:00
8b71c150c6 Bump pytest from 6.0.1 to 6.0.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.1 to 6.0.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/6.0.1...6.0.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-14 06:39:11 +00:00
dcce0fa3df Merge pull request #520 from skuhl/master
Fix #494 sshuttle caught in infinite select() loop.
2020-09-09 08:37:27 +10:00
3ee74ddfa6 Merge pull request #519 from sshuttle/dependabot/pip/attrs-20.2.0
Bump attrs from 20.1.0 to 20.2.0
2020-09-08 07:33:26 +10:00
7573011a08 remove unneeded blank line 2020-09-07 15:58:59 -04:00
72ae202df3 Remove extra whitespace, wrap long lines 2020-09-07 15:56:59 -04:00
5d6b14673f Fix #494 sshuttle caught in infinite select() loop.
Improve detection of when the ssh process exits in both daemon and
foreground modes. Previously, sshuttle could infinite loop with 100%
cpu usage if the ssh process died. On machines that use suspend, the
ssh connection might not resume after wakeup. Now, this situation is
detected and sshuttle exits. The fix involves changing the return
value we check for when we call poll() and using a psutil function to
detect when the process exits if we are running sshuttle as a daemon.
2020-09-07 15:46:33 -04:00
aa97742405 Bump attrs from 20.1.0 to 20.2.0
Bumps [attrs](https://github.com/python-attrs/attrs) from 20.1.0 to 20.2.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/20.1.0...20.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-07 06:46:51 +00:00
19f653df36 Merge pull request #513 from drjbarker/python2-compat
Fix python2 server compatibility
2020-08-30 09:48:29 +10:00
ec5fb68350 Fix python2 client compatibility
Python2 ignores the byte string qualification (b’foo’)  but falls over for the combination rb for this regexp. Switching the qualification to br appears to fix this and works in both python2 and python3.
2020-08-29 21:32:18 +09:00
d9e5ccc19c Merge pull request #512 from xoro/master
Fixed typo.
2020-08-28 16:04:49 +10:00
f23510a4fc Fix Codacy check redefined-argument-from-local 2020-08-28 10:37:20 +09:00
459e573019 Fix flake8 line too long 2020-08-28 10:29:12 +09:00
c12d2ba5c6 Fix python2 server compatibility
Fixes  #469. We replace python3 exclusive code with a check for python3 and a compatibility fix. Note that the switch from os.set_nonblocking to fcntl.fcntl in 98d052d (fixing #503) also fixes python2 compatibility.
2020-08-28 10:04:12 +09:00
630f8c2357 Fixed typo. 2020-08-27 20:59:37 +02:00
e8f3b53c7d Merge pull request #511 from Rylan12/license-update
Change license text to LGPL-2.1
2020-08-27 08:03:26 +10:00
8ee230bca7 Change license text to LGPL-2.1 2020-08-26 12:25:36 -04:00
abb48f1996 Update changes file 2020-08-24 08:00:36 +10:00
1c27a6cad0 Merge pull request #510 from sshuttle/dependabot/pip/attrs-20.1.0
Bump attrs from 19.3.0 to 20.1.0
2020-08-21 16:42:05 +10:00
8a2d5802c1 Bump attrs from 19.3.0 to 20.1.0
Bumps [attrs](https://github.com/python-attrs/attrs) from 19.3.0 to 20.1.0.
- [Release notes](https://github.com/python-attrs/attrs/releases)
- [Changelog](https://github.com/python-attrs/attrs/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/python-attrs/attrs/compare/19.3.0...20.1.0)

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

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

* Broke the line because it was too long.
2020-08-05 20:28:36 +10:00
5bdf36152a Merge pull request #498 from sshuttle/dependabot/pip/pytest-6.0.1
Bump pytest from 6.0.0 to 6.0.1
2020-08-01 18:07:00 +10:00
a9ee66d905 Bump pytest from 6.0.0 to 6.0.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.0.0...6.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-31 08:26:00 +00:00
094d3d9b97 Merge pull request #497 from sshuttle/dependabot/pip/pytest-6.0.0
Bump pytest from 5.4.3 to 6.0.0
2020-07-31 07:57:58 +10:00
19b677892e Bump pytest from 5.4.3 to 6.0.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.3 to 6.0.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.4.3...6.0.0)

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

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

Closes #470

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

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

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

* use source loader from importlib

* Revert "use source loader from importlib"

This reverts commit 1f255704f7.

* use inspect.getsource, but alas

* placate linter

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

* better function naming

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

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-09 18:48:45 +10:00
8c9dad1c6b Fix errors in long_documentation 2020-06-05 08:19:24 +10:00
7fde5b6fa6 Release version 1.0.0 2020-06-05 08:13:23 +10:00
734168531f Update changelog 2020-06-05 08:05:41 +10:00
d058d9bc93 Bump pytest from 5.4.2 to 5.4.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.4.2...5.4.3)

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

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

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

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

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

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

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

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

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

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

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

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

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

* nicer formatting, pep8 applied

* Properly parse IPv6 addresses with port specification

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

* Updated parameter description for the remotehost specification

* Make the urlparse import backwards compatible to python2

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

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

* Update ssh.py

* Fix for IPv4 only

* Delete print sorry bad commit

* ipv4 fix

* Fix IPv4 args

* Fix for ipv6

* Fix ipv6 no password

* Add function parse_hostport

* Fix minor bug detect port

* Fix minor bug password detect

* Clear Code

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

* Rewrite code for more understand logical and fix minor bug

* add default define port

* delete old variable unused

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

* reback

* try define password with len

* Fix consistence variable password PR

* simplify function split ipv4 or ipv6

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

* added sudoers options to command line arguments

* template for sudoers file

* Added option for GUI sudo

* added support for GUI sudo

* script for auto adding sudo file

* sudoers auto add works and validates

* small change

* Clean up for CI

* removed code that belongs in another PR

* added path for package bins

* added sudoers bin

* added sudoers-add to setup file

* fixed issue with sudoers bash script

* auto sudoers now works

* added --sudoers-no-modify option

* bin now works with ./run

* removed debug print

* Updated sudoers-add script

* Fixed error passing sudoers config to script

* more dynamic building of sudoers file

* added option to specify sudoers.d file name

* fixed indent issue

* fixed indent issue

* indent issue

* clean up

* formating

* docs

* fix for flags

* Update usage.rst

* removed shell=true

* cleared CI errors

* cleared CI errors

* removed random

* cleared linter issue

* cleared linter issue

* cleared linter issue

* updated sudoers-add script

* safer temp file

* moved bin directory

* moved bin directory

* removed print

* fixed spacing issue

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

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

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

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

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

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

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

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

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

* Fix linter complaints
2019-06-08 12:12:21 +10:00
635cf8605e Add install instructions for Fedora 2019-06-08 10:34:53 +10:00
cb917d7e6c Add install instructions for Arch Linux 2019-04-04 12:31:10 +11:00
4372c6c117 Hyphen in hostname fix 2019-02-14 14:14:56 +11:00
4e945ca4de assembler import fix (#319)
* assembler import fix.
* Added noqa to import statements.
2019-02-14 12:11:11 +11:00
3bfb975ed9 Fix/pep8 (#277)
* re-organized imports according to pep8
* fixed all remaining pep8 issues
* moved common config into setup.cfg, additionally test `tests`
* removed --select=X -- the errors selected where by default not in
  flake8's --ignore list so effectively had no effect
* update .travis.yml to reflect changes in tox.ini
* make travis just use tox in order to avoid code duplaction
* replace py.test with pytest
* fixed .travis.yml
* try different pypy toxenv
* hopefully fixed testenv for pypy
* added pypy basepython, removed unused python2.6
* install dev package before testing (fixes missing coverage)
* fixed empty exception pass blocks with noqa
* Added dummy log message on empty try-except-pass blocks to make dodacy happy :(
* Replaced Exception with BaseException
2019-02-11 09:59:13 +11:00
752a953101 Release 0.78.5 2019-01-28 16:28:57 +13:00
61f4cd9de5 Update CHANGES.rst for new release 2019-01-28 11:36:14 +13:00
8e35f049e2 auto-nets: retrieve routes only if using auto-nets
There's a known issue that makes sshuttle crash if there are too
many routes on the remote host (that don't fit in 64KB). This patch
requests the routes only if auto-nets is specified on the command
line.
2019-01-28 08:53:51 +13:00
0e99adc5d1 Fix potential deadlock condition in nft_get_handle
This was susceptible to the same deadlock issue that ipt_chain_exists
had and was fixed in d43db80 where if the command returned a significant
amount of output, it wouldn't all be read in, resulting in the
subprocess hanging waiting for the output to be read.
2019-01-23 18:53:45 +13:00
04849df7e3 Use subprocess.check_output instead of run
subprocess.run only exists for python3, and this needs to also support
python 2.7
2019-01-23 18:53:45 +13:00
531a17c151 docs: document --ns-hosts --to-ns and update --dns
--ns-hosts is available since commit d2ee34d71c
("dns: Added --ns-hosts to tunnel only some requests")
(released as v0.72), but was never documented.

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

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

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

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

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

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

Fixes #139

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

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

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

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

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

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

* crostini doc, and a note about xterms and VNC

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

Fixes #259
2018-08-21 08:36:51 +10:00
7b6f082454 Doc Update
Remove reference to autossh per
https://github.com/sshuttle/sshuttle/issues/143
2018-06-29 07:38:55 +10:00
1ec17e1b1b Update README.rst 2018-06-22 16:02:11 +10:00
cecccc2efd Doc: Fix typo 2018-06-07 07:00:17 +10:00
db69ba6d8d Doc: Improve Systemd service recommendation (fixes #238) 2018-06-03 07:38:21 +10:00
2bb92cd6d4 Fix sudo/doas
Fixes #227
2018-05-13 20:35:18 +10:00
ae5bd28dcf Add doas support for client 2018-05-09 17:46:33 +10:00
55bd78fd43 Fix line length for CI. 2018-04-30 07:40:58 +10:00
1f5ed9c66e Fix concatening string to tuple. Allow for forwarding a single port. 2018-04-30 07:40:58 +10:00
58 changed files with 2929 additions and 1512 deletions

12
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

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

@ -0,0 +1,37 @@
# 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 ]
workflow_dispatch:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
steps:
- uses: actions/checkout@v2.4.0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2.3.1
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

1
.gitignore vendored
View File

@ -15,3 +15,4 @@
/.redo
/.pytest_cache/
/.python-version
.vscode/

15
.readthedocs.yaml Normal file
View File

@ -0,0 +1,15 @@
version: 2
build:
os: ubuntu-20.04
tools:
python: "3.9"
sphinx:
configuration: docs/conf.py
python:
install:
- requirements: requirements.txt
- method: setuptools
path: .

View File

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

View File

@ -1,12 +1,157 @@
==========
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`_.
Release notes now moved to https://github.com/sshuttle/sshuttle/releases/
.. _`Keep a Changelog`: http://keepachangelog.com/
.. _`Semantic Versioning`: http://semver.org/
These are the old release notes.
1.0.5 - 2020-12-29
------------------
Added
~~~~~
* IPv6 support in nft method.
* Intercept DNS requests sent by systemd-resolved.
* Set default tmark.
* Fix python2 server compatibility.
* Python 3.9 support.
Fixed
~~~~~
* Change license text to LGPL-2.1
* Fix #494 sshuttle caught in infinite select() loop.
* Include sshuttle version in verbose output.
* Add psutil as dependency in setup.py
* When subnets and excludes are specified with hostnames, use all IPs.
* Update/document client's handling of IPv4 and IPv6.
* Update sdnotify.py documentation.
* Allow no remote to work.
* Make prefixes in verbose output more consistent.
* Make nat and nft rules consistent; improve rule ordering.
* Make server and client handle resolv.conf differently.
* Fix handling OSError in FirewallClient#__init__
* Refactor automatic method selection.
Removed
~~~~~~~
* Drop testing of Python 3.5
1.0.4 - 2020-08-24
------------------
Fixed
~~~~~
* Allow Mux() flush/fill to work with python < 3.5
* Fix parse_hostport to always return string for host.
* Require -r/--remote parameter.
* Add missing package in OpenWRT documentation.
* Fix doc about --listen option.
* README: add Ubuntu.
* Increase IP4 ttl to 63 hops instead of 42.
* Fix formatting in installation.rst
1.0.3 - 2020-07-12
------------------
Fixed
~~~~~
* Ask setuptools to require Python 3.5 and above.
* Add missing import.
* Fix formatting typos in usage docs
1.0.2 - 2020-06-18
------------------
Fixed
~~~~~
* Leave use of default port to ssh command.
* Remove unwanted references to Python 2.7 in docs.
* Replace usage of deprecated imp.
* Fix connection with @ sign in username.
1.0.1 - 2020-06-05
------------------
Fixed
~~~~~
* Errors in python long_documentation.
1.0.0 - 2020-06-05
------------------
Added
~~~~~
* Python 3.8 support.
* sshpass support.
* Auto sudoers file (#269).
* option for latency control buffer size.
* Docs: FreeBSD'.
* Docs: Nix'.
* Docs: openwrt'.
* Docs: install instructions for Fedora'.
* Docs: install instructions for Arch Linux'.
* Docs: 'My VPN broke and need a solution fast'.
Removed
~~~~~~~
* Python 2.6 support.
* Python 2.7 support.
Fixed
~~~~~
* Remove debug message for getpeername failure.
* Fix crash triggered by port scans closing socket.
* Added "Running as a service" to docs.
* Systemd integration.
* Trap UnicodeError to handle cases where hostnames returned by DNS are invalid.
* Formatting error in CHANGES.rst
* Various errors in documentation.
* Nftables based method.
* Make hostwatch locale-independent (#379).
* Add tproxy udp port mark filter that was missed in #144, fixes #367.
* Capturing of local DNS servers.
* Crashing on ECONNABORTED.
* Size of pf_rule, which grew in OpenBSD 6.4.
* Use prompt for sudo, not needed for doas.
* Arch linux installation instructions.
* tests for existing PR-312 (#337).
* Hyphen in hostname.
* Assembler import (#319).
0.78.5 - 2019-01-28
-------------------
Added
~~~~~
* doas support as replacement 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

191
LICENSE
View File

@ -1,13 +1,14 @@
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991 Free Software Foundation, Inc.
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
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.]
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
@ -16,97 +17,109 @@ freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Library General Public License, applies to some
specially designated Free Software Foundation software, and to any
other libraries whose authors decide to use it. You can use it for
your libraries, too.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if
you distribute copies of the library, or if you modify it.
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link a program with the library, you must provide
complete object files to the recipients so that they can relink them
with the library, after making changes to the library and recompiling
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
Our method of protecting your rights has two steps: (1) copyright
the library, and (2) offer you this license which gives you legal
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
Also, for each distributor's protection, we want to make certain
that everyone understands that there is no warranty for this free
library. If the library is modified by someone else and passed on, we
want its recipients to know that what they have is not the original
version, so that any problems introduced by others will not reflect on
the original authors' reputations.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that companies distributing free
software will individually obtain patent licenses, thus in effect
transforming the program into proprietary software. To prevent this,
we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the ordinary
GNU General Public License, which was designed for utility programs. This
license, the GNU Library General Public License, applies to certain
designated libraries. This license is quite different from the ordinary
one; be sure to read it in full, and don't assume that anything in it is
the same as in the ordinary license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
The reason we have a separate public license for some libraries is that
they blur the distinction we usually make between modifying or adding to a
program and simply using it. Linking a program with a library, without
changing the library, is in some sense simply using the library, and is
analogous to running a utility program or application program. However, in
a textual and legal sense, the linked executable is a combined work, a
derivative of the original library, and the ordinary General Public License
treats it as such.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
Because of this blurred distinction, using the ordinary General
Public License for libraries did not effectively promote software
sharing, because most developers did not use the libraries. We
concluded that weaker conditions might promote sharing better.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
However, unrestricted linking of non-free programs would deprive the
users of those programs of all benefit from the free status of the
libraries themselves. This Library General Public License is intended to
permit developers of non-free programs to use free libraries, while
preserving your freedom as a user of such programs to change the free
libraries that are incorporated in them. (We have not seen how to achieve
this as regards changes in header files, but we have achieved it as regards
changes in the actual functions of the Library.) The hope is that this
will lead to faster development of free libraries.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, while the latter only
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.
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LIBRARY GENERAL PUBLIC LICENSE
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which
contains a notice placed by the copyright holder or other authorized
party saying it may be distributed under the terms of this Library
General Public License (also called "this License"). Each licensee is
addressed as "you".
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
@ -255,7 +268,7 @@ distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also compile or
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
@ -282,23 +295,31 @@ of these things:
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Accompany the work with a written offer, valid for at
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
d) Verify that the user has already received a copy of these
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the source code distributed need not include anything that is normally
distributed (in either source or binary form) with the major
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
@ -347,7 +368,7 @@ Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
@ -390,7 +411,7 @@ excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Library General Public License from time to time.
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
@ -453,16 +474,16 @@ convey the exclusion of warranty; and each file should have at least the
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
version 2.1 of the License, or (at your option) any later version.
This library 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
Library General Public License for more details.
Lesser General Public License for more details.
You should have received a copy of the GNU Library General Public
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

View File

@ -23,16 +23,41 @@ common case:
- You can't use openssh's PermitTunnel feature because
it's disabled by default on openssh servers; plus it does
TCP-over-TCP, which has terrible performance (see below).
TCP-over-TCP, which has `terrible performance`_.
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
Obtaining sshuttle
------------------
- Ubuntu 16.04 or later::
apt-get install sshuttle
- Debian stretch or later::
apt-get install sshuttle
- Arch Linux::
pacman -S sshuttle
- Fedora::
dnf install sshuttle
- openSUSE::
zypper in sshuttle
- Gentoo::
emerge -av net-proxy/sshuttle
- NixOS::
nix-env -iA nixos.sshuttle
- From PyPI::
sudo pip install sshuttle
@ -43,6 +68,18 @@ Obtaining sshuttle
cd sshuttle
sudo ./setup.py install
- FreeBSD::
# ports
cd /usr/ports/net/py-sshuttle && make install clean
# pkg
pkg install py36-sshuttle
- macOS, via MacPorts::
sudo port selfupdate
sudo port install sshuttle
It is also possible to install into a virtualenv as a non-root user.
- From PyPI::
@ -63,11 +100,21 @@ It is also possible to install into a virtualenv as a non-root user.
brew install sshuttle
- Nix::
nix-env -iA nixpkgs.sshuttle
Documentation
-------------
The documentation for the stable version is available at:
http://sshuttle.readthedocs.org/
https://sshuttle.readthedocs.org/
The documentation for the latest development version is available at:
http://sshuttle.readthedocs.org/en/latest/
https://sshuttle.readthedocs.org/en/latest/
Running as a service
--------------------
Sshuttle can also be run as a service and configured using a config management system:
https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe

View File

@ -1,5 +1,5 @@
exclude_dirs:
- sshuttle/tests
- tests
skips:
- B101
- B104

84
bin/sudoers-add Executable file
View File

@ -0,0 +1,84 @@
#!/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
# Verify that the resulting file name begins with /etc/sudoers.d
FILE_NAME="$(realpath "/etc/sudoers.d/$FILE_NAME")"
if [[ "$FILE_NAME" != "/etc/sudoers.d/"* ]] ; then
echo -n "Invalid sudoers filename: Final sudoers file "
echo "location ($FILE_NAME) does not begin with /etc/sudoers.d"
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" > "$FILE_NAME"
chmod 0440 "$FILE_NAME"
echo "The sudoers file $FILE_NAME has been successfully created!"
exit 0
else
echo "Invalid sudoers config!"
echo "$visudo_STDOUT"
exit 1
fi

View File

@ -1,10 +0,0 @@
import sys
if sys.version_info >= (3, 0):
good_python = sys.version_info >= (3, 5)
else:
good_python = sys.version_info >= (2, 7)
collect_ignore = []
if not good_python:
collect_ignore.append("sshuttle/tests/client")

11
docs/chromeos.rst Normal file
View File

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

View File

@ -34,4 +34,3 @@ 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.

View File

@ -26,4 +26,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`search`

View File

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

View File

@ -4,14 +4,14 @@ sshuttle
Synopsis
--------
**sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\>
**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 2.3 or higher.
machine to any remote server that you can connect to via ssh, as long
as that server has a sufficiently new Python installation.
To work, you must have root access on the local machine,
but you can have a normal account on the server.
@ -28,29 +28,34 @@ Options
-------
.. program:: sshuttle
.. option:: subnets
.. 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.
single IP address) and 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).
Specify subnets 0/0 to match all IPv4 addresses and ::/0 to match
all IPv6 addresses. 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. A hostname can be provided instead of an IP address. If
the hostname resolves to multiple IPs, all of the IPs are
included. If a width is provided with a hostname, the width is
applied to all of the hostnames IPs (if they are all either IPv4
or IPv6). Widths cannot be supplied to hostnames that resolve to
both IPv4 and IPv6. Valid examples are example.com,
example.com:8000, example.com/24, example.com/24:8000 and
example.com:8000-9000.
.. option:: --method [auto|nat|tproxy|pf]
.. option:: --method <auto|nat|nft|tproxy|pf|ipfw>
Which firewall method should sshuttle use? For auto, sshuttle attempts to
guess the appropriate method depending on what it can find in PATH. The
default value is auto.
.. option:: -l, --listen=[ip:]port
.. option:: -l <[ip:]port>, --listen=<[ip:]port>
Use this ip address and port number as the transparent
proxy port. By default :program:`sshuttle` finds an available
@ -64,8 +69,9 @@ Options
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.
For the nft, tproxy and pf methods this can be an IPv6 address. Use
this option with comma separated values if required, to provide both
IPv4 and IPv6 addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``.
.. option:: -H, --auto-hosts
@ -83,6 +89,13 @@ Options
few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else.
:program:`sshuttle` tries to store a cache of the hostnames in
~/.sshuttle.hosts on the remote host. Similarly, it tries to read
the file when you later reconnect to the host with --auto-hosts
enabled to quickly populate the host list. When troubleshooting
this feature, try removing this file on the remote host when
sshuttle is not running.
.. option:: -N, --auto-nets
In addition to the subnets provided on the command
@ -91,25 +104,57 @@ Options
are taken automatically from the server's routing
table.
This feature does not detect IPv6 routes. Specify IPv6 subnets
manually. For example, specify the ``::/0`` subnet on the command
line to route all IPv6 traffic.
.. option:: --dns
Capture local DNS requests and forward to the remote DNS
server.
server. All queries to any of the local system's DNS
servers (/etc/resolv.conf and, if it exists,
/run/systemd/resolve/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. Only plain DNS traffic sent to these servers
on port 53 are captured.
.. 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.
Specify the name/path of the remote python interpreter. The
default is to use ``python3`` (or ``python``, if ``python3``
fails) in the remote system's PATH.
.. option:: -r, --remote=[username@]sshserver[:port]
.. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
The remote hostname and optional username and ssh
port number to use for connecting to the remote server.
For example, example.com, testuser@example.com,
testuser@example.com:2222, or example.com:2244.
testuser@example.com:2222, or example.com:2244. This
hostname is passed to ssh, so it will recognize any
aliases and settings you may have configured in
~/.ssh/config.
.. option:: -x, --exclude=subnet
.. option:: -x <subnet>, --exclude=<subnet>
Explicitly exclude this subnet from forwarding. The
format of this option is the same as the ``<subnets>``
@ -118,7 +163,7 @@ Options
``0/0 -x 1.2.3.0/24`` to forward everything except the
local subnet over the VPN, for example.
.. option:: -X, --exclude-from=file
.. option:: -X <file>, --exclude-from=<file>
Exclude the subnets specified in a file, one subnet per
line. Useful when you have lots of subnets to exclude.
@ -140,7 +185,7 @@ Options
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
:option:`--auto-hosts` does things like poll netstat output
for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start
from.
@ -166,18 +211,30 @@ Options
control feature, maximizing bandwidth usage. Use at
your own risk.
.. option:: --latency-buffer-size
Set the size of the buffer used in latency control. The
default is ``32768``. Changing this option allows a compromise
to be made between latency and bandwidth without completely
disabling latency control (with :option:`--no-latency-control`).
.. option:: -D, --daemon
Automatically fork into the background after connecting
to the remote server. Implies :option:`--syslog`.
.. option:: -s <file>, --subnets=<file>
Include the subnets specified in a file instead of on the
command line. One subnet per line.
.. 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
.. option:: --pidfile=<pidfilename>
when using :option:`--daemon`, save :program:`sshuttle`'s pid to
*pidfilename*. The default is ``sshuttle.pid`` in the
@ -185,7 +242,8 @@ Options
.. option:: --disable-ipv6
If using tproxy or pf methods, this will disable IPv6 support.
Disable IPv6 support for methods that support it (nft, tproxy, and
pf).
.. option:: --firewall
@ -204,17 +262,49 @@ Options
makes it a lot easier to debug and test the :option:`--auto-hosts`
feature.
.. option:: --sudoers
sshuttle will auto generate the proper sudoers.d config file and add it.
Once this is completed, sshuttle will exit and tell the user if
it succeed or not. Do not call this options with sudo, it may generate a
incorrect config file.
.. option:: --sudoers-no-modify
sshuttle will auto generate the proper sudoers.d config and print it to
stdout. The option will not modify the system at all.
.. option:: --sudoers-user
Set the user name or group with %group_name for passwordless operation.
Default is the current user.set ALL for all users. Only works with
--sudoers or --sudoers-no-modify option.
.. option:: --sudoers-filename
Set the file name for the sudoers.d file to be added. Default is
"sshuttle_auto". Only works with --sudoers.
.. option:: -t <mark>, --tmark=<mark>
An option used by the tproxy method: Use the specified traffic
mark. The mark must be a hexadecimal value. Defaults to 0x01.
.. 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/ssshuttle.conf`
To run :program:`sshuttle` with options defined in, e.g., `/etc/sshuttle.conf`
just pass the path to the file preceded by the `@` character, e.g.
:option:`@/etc/ssshuttle.conf`.
`@/etc/sshuttle.conf`.
When running :program:`sshuttle` with options defined in a configuratio file,
When running :program:`sshuttle` with options defined in a configuration file,
options can still be passed via the command line in addition to what is
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.
@ -227,54 +317,107 @@ Arguments read from a file must be one per line, as shown below::
--option2
value2
The configuration file supports comments for human-readable
annotations. For example::
# company-internal API
8.8.8.8/32
# home IoT
192.168.63.0/24
Examples
--------
Test locally by proxying all local connections, without using ssh::
$ sshuttle -v 0/0
Use the following command to route all IPv4 TCP traffic through remote
(-r) host example.com (and possibly other traffic too, depending on
the selected --method). The 0/0 subnet, short for 0.0.0.0/0, matches
all IPv4 addresses. The ::/0 subnet, matching all IPv6 addresses could
be added to the example. We also exclude (-x) example.com:22 so that
we can establish ssh connections from our local machine to the remote
host without them being routed through sshuttle. Excluding the remote
host may be necessary on some machines for sshuttle to work properly.
Press Ctrl+C to exit. To also route DNS queries through sshuttle, try
adding --dns. Add or remove -v options to see more or less
information::
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
$ sshuttle -r example.com -x example.com:22 0/0
Starting sshuttle proxy (version ...).
[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...
fw: Starting firewall with Python version 3.9.5
fw: ready method name nat.
c : IPv6 disabled since it isn't supported by method nat.
c : Method: nat
c : IPv4: on
c : IPv6: off (not available with nat method)
c : UDP : off (not available with nat method)
c : DNS : off (available)
c : User: off (available)
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
c : (<AddressFamily.AF_INET: 2>, '0.0.0.0', 0, 0, 0)
c : Subnets to exclude from forwarding:
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
c : TCP redirector listening on ('127.0.0.1', 12299).
c : Starting client with Python version 3.9.5
c : Connecting to server...
user@example.com's password:
s: Starting server with Python version 3.6.8
s: latency control setting = True
s: auto-nets:False
c : Connected to server.
fw: setting up.
fw: iptables -w -t nat -N sshuttle-12299
fw: iptables -w -t nat -F sshuttle-12299
...
Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
^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
c : SW'unknown':Mux#1: deleting (1 remain)
c : SW#7:192.168.42.121:60554: deleting (0 remain)
Test connection to a remote server, with automatic hostname
Connect 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...
$ sshuttle -vNHr example.com -x example.com:22
Starting sshuttle proxy (version ...).
[local sudo] Password:
fw: Starting firewall with Python version 3.9.5
fw: ready method name nat.
c : IPv6 disabled since it isn't supported by method nat.
c : Method: nat
c : IPv4: on
c : IPv6: off (not available with nat method)
c : UDP : off (not available with nat method)
c : DNS : off (available)
c : User: off (available)
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
c : NOTE: Additional subnets to forward may be added below by --auto-nets.
c : Subnets to exclude from forwarding:
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
c : TCP redirector listening on ('127.0.0.1', 12300).
c : Starting client with Python version 3.9.5
c : Connecting to server...
user@example.com's password:
s: Starting server with Python version 3.6.8
s: latency control setting = True
s: auto-nets:True
c : Connected to server.
c : seed_hosts: []
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
fw: setting up.
fw: iptables -w -t nat -N sshuttle-12300
fw: iptables -w -t nat -F sshuttle-12300
...
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
c : SW'unknown':Mux#1: deleting (1 remain)
c : SW#7:192.168.42.121:60554: deleting (0 remain)
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
@ -298,9 +441,7 @@ Example configuration file::
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.
server specified by the ``-r`` option.
After connecting to the remote server, :program:`sshuttle` uploads its
(python) source code to the remote end and executes it

8
docs/openwrt.rst Normal file
View File

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

View File

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

View File

@ -6,7 +6,7 @@ Client side Requirements
- sudo, or root access on your client machine.
(The server doesn't need admin access.)
- Python 2.7 or Python 3.5.
- Python 3.6 or greater.
Linux with NAT method
@ -15,30 +15,36 @@ Supports:
* IPv4 TCP
* IPv4 DNS
* IPv6 TCP
* IPv6 DNS
Requires:
* iptables DNAT, REDIRECT, and ttl modules.
* iptables DNAT and REDIRECT modules. ip6tables for IPv6.
Linux with nft method
~~~~~~~~~~~~~~~~~~~~~
Supports
* IPv4 TCP
* IPv4 DNS
* IPv6 TCP
* IPv6 DNS
Requires:
* nftables
Linux with TPROXY method
~~~~~~~~~~~~~~~~~~~~~~~~
Supports:
* IPv4 TCP
* IPv4 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
* IPv4 UDP
* IPv4 DNS
* IPv6 TCP
* IPv6 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
.. _PyXAPI: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
Full UDP or DNS support with the TPROXY method requires the ``recvmsg()``
syscall. This is not available in Python 2, however it is in Python 3.5 and
later. Under Python 2 you might find it sufficient to install PyXAPI_ in
order to get the ``recvmsg()`` function. See :doc:`tproxy` for more
information.
* IPv6 UDP
* IPv6 DNS
MacOS / FreeBSD / OpenBSD / pfSense
@ -65,16 +71,13 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
Server side Requirements
------------------------
The server can run in any version of Python between 2.4 and 3.6.
However it is recommended that you use Python 2.7, Python 3.5 or later whenever
possible as support for older versions might be dropped in the future.
- Python 3.6 or greater.
Additional Suggested Software
-----------------------------
- You may want to use autossh, available in various package management
systems.
- If you are using systemd, sshuttle can notify it when the connection to
the remote end is established and the firewall rules are installed. For
this feature to work you must configure the process start-up type for the

View File

@ -1,6 +1,6 @@
TPROXY
======
TPROXY is the only method that has full support of IPv6 and UDP.
TPROXY is the only method that supports UDP.
There are some things you need to consider for TPROXY to work:
@ -8,9 +8,12 @@ There are some things you need to consider for TPROXY to work:
done once after booting up::
ip route add local default dev lo table 100
ip rule add fwmark 1 lookup 100
ip rule add fwmark {TMARK} lookup 100
ip -6 route add local default dev lo table 100
ip -6 rule add fwmark 1 lookup 100
ip -6 rule add fwmark {TMARK} lookup 100
where {TMARK} is the identifier mark passed with -t or --tmark flag
as a hexadecimal string (default value is '0x01').
- 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.
@ -23,11 +26,6 @@ There are some things you need to consider for TPROXY to work:
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

View File

@ -33,4 +33,3 @@ 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.

View File

@ -11,6 +11,10 @@ Forward all traffic::
sshuttle -r username@sshserver 0.0.0.0/0
- Use the :option:`sshuttle -r` parameter to specify a remote server.
One some systems, you may also need to use the :option:`sshuttle -x`
parameter to exclude sshserver or sshserver:22 so that your local
machine can communicate directly to sshserver without it being
redirected by sshuttle.
- By default sshuttle will automatically choose a method to use. Override with
the :option:`sshuttle --method` parameter.
@ -20,6 +24,11 @@ Forward all traffic::
sshuttle -r username@sshserver 0/0
- For 'My VPN broke and need a temporary solution FAST to access local IPv4 addresses'::
sshuttle --dns -NHr username@sshserver 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
If you would also like your DNS queries to be proxied
through the DNS server of the server you are connect to::
@ -60,3 +69,46 @@ the data back and forth through ssh.
Fun, right? A poor man's instant VPN, and you don't even have to have
admin access on the server.
Sudoers File
------------
sshuttle can auto-generate the proper sudoers.d file using the current user
for Linux and OSX. Doing this will allow sshuttle to run without asking for
the local sudo password and to give users who do not have sudo access
ability to run sshuttle::
sshuttle --sudoers
DO NOT run this command with sudo, it will ask for your sudo password when
it is needed.
A costume user or group can be set with the :
option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid
values for this vary based on how your system is configured. Values such as
usernames, groups pre-pended with `%` and sudoers user aliases will work. See
the sudoers manual for more information on valid user specif actions.
The options must be used with `--sudoers`::
sshuttle --sudoers --sudoers-user mike
sshuttle --sudoers --sudoers-user %sudo
The name of the file to be added to sudoers.d can be configured as well. This
is mostly not necessary but can be useful for giving more than one user
access to sshuttle. The default is `sshuttle_auto`::
sshuttle --sudoer --sudoers-filename sshuttle_auto_mike
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
You can also see what configuration will be added to your system without
modifying anything. This can be helpful if the auto feature does not work, or
you want more control. This option also works with `--sudoers-username`.
`--sudoers-filename` has no effect with this option::
sshuttle --sudoers-no-modify
This will simply sprint the generated configuration to STDOUT. Example::
08:40 PM william$ sshuttle --sudoers-no-modify
Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall
william ALL=NOPASSWD: SSHUTTLE304

View File

@ -1,4 +1,5 @@
-r requirements.txt
pytest==3.4.2
mock==2.0.0
flake8==3.5.0
pytest==6.2.5
pytest-cov==3.0.0
flake8==4.0.1
pyflakes==2.4.0

View File

@ -1 +1,2 @@
setuptools-scm==1.15.6
setuptools-scm==6.4.2
Sphinx==4.3.2

5
run
View File

@ -1,13 +1,12 @@
#!/usr/bin/env sh
set -e
export PYTHONPATH="$(dirname $0):$PYTHONPATH"
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 "$@"
elif [ -x "$(command -v python2.7)" ]; then
exec python2.7 "$@"
else
exec python "$@"
fi

View File

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

View File

@ -41,6 +41,7 @@ setup(
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",
@ -48,15 +49,26 @@ setup(
"License :: OSI Approved :: "
"GNU Lesser General Public License v2 or later (LGPLv2+)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Topic :: System :: Networking",
],
scripts=['bin/sudoers-add'],
entry_points={
'console_scripts': [
'sshuttle = sshuttle.cmdline:main',
],
},
tests_require=['pytest', 'pytest-runner', 'mock'],
python_requires='>=3.6',
install_requires=[
],
tests_require=[
'pytest',
'pytest-cov',
'pytest-runner',
'flake8',
],
keywords="ssh vpn",
)

View File

@ -1,21 +1,28 @@
import sys
import zlib
import imp
import types
import platform
verbosity = verbosity # noqa: F821 must be a previously defined global
if verbosity > 0:
sys.stderr.write(' s: Running server on remote host with %s (version %s)\n'
% (sys.executable, platform.python_version()))
z = zlib.decompressobj()
while 1:
name = sys.stdin.readline().strip()
if name:
# python2 compat: in python2 sys.stdin.readline().strip() -> str
# in python3 sys.stdin.readline().strip() -> bytes
# (see #481)
if sys.version_info >= (3, 0):
name = name.decode("ASCII")
nbytes = int(sys.stdin.readline())
if verbosity >= 2:
sys.stderr.write('server: assembling %r (%d bytes)\n'
sys.stderr.write(' s: assembling %r (%d bytes)\r\n'
% (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes))
module = imp.new_module(name)
module = types.ModuleType(name)
parents = name.rsplit(".", 1)
if len(parents) == 2:
parent, parent_name = parents
@ -30,9 +37,13 @@ while 1:
sys.stderr.flush()
sys.stdout.flush()
import sshuttle.helpers
# import can only happen once the code has been transferred to
# the server. 'noqa: E402' excludes these lines from QA checks.
import sshuttle.helpers # noqa: E402
sshuttle.helpers.verbose = verbosity
import sshuttle.cmdline_options as options
from sshuttle.server import main
main(options.latency_control, options.auto_hosts, options.to_nameserver)
import sshuttle.cmdline_options as options # noqa: E402
from sshuttle.server import main # noqa: E402
main(options.latency_control, options.latency_buffer_size,
options.auto_hosts, options.to_nameserver,
options.auto_nets)

View File

@ -3,44 +3,36 @@ import re
import signal
import time
import subprocess as ssubprocess
import sshuttle.helpers as helpers
import os
import sys
import platform
import sshuttle.helpers as helpers
import sshuttle.ssnet as ssnet
import sshuttle.ssh as ssh
import sshuttle.ssyslog as ssyslog
import sys
import platform
import sshuttle.sdnotify as sdnotify
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
resolvconf_nameservers
resolvconf_nameservers, which
from sshuttle.methods import get_method, Features
from sshuttle import __version__
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('/dev/null', os.O_RDONLY)
_extra_fd = os.open(os.devnull, os.O_RDONLY)
def got_signal(signum, frame):
log('exiting on signal %d\n' % signum)
log('exiting on signal %d' % signum)
sys.exit(1)
# Filename of the pidfile created by the sshuttle client.
_pidname = None
@ -76,13 +68,25 @@ def check_daemon(pidfile):
def daemonize():
# Try to open the pidfile prior to forking. If there is a problem,
# the client can then exit with a proper exit status code and
# message.
try:
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
except PermissionError:
# User will have to look in syslog for error message since
# --daemon implies --syslog, all output gets redirected to
# syslog.
raise Fatal("failed to create/write pidfile %s" % _pidname)
# Create a daemon process with a new session id.
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)
# Write pid to the pidfile.
try:
os.write(outfd, b'%d\n' % os.getpid())
finally:
@ -93,7 +97,7 @@ def daemonize():
# be deleted.
signal.signal(signal.SIGTERM, got_signal)
si = open('/dev/null', 'r+')
si = open(os.devnull, 'r+')
os.dup2(si.fileno(), 0)
os.dup2(si.fileno(), 1)
si.close()
@ -148,7 +152,7 @@ class MultiListener:
try:
self.v4.listen(backlog)
except socket.error as e:
# on some systems v4 bind will fail if the v6 suceeded,
# on some systems v4 bind will fail if the v6 succeeded,
# in this case the v6 socket will receive v4 too.
if e.errno == errno.EADDRINUSE and self.v6:
self.v4 = None
@ -173,74 +177,125 @@ class MultiListener:
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))
debug1('%s listening on %r.' % (what, listenip))
debug2('%s listening with %r.' % (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))
debug1('%s listening on %r.' % (what, listenip))
debug2('%s listening with %r.' % (what, self.v4))
class FirewallClient:
def __init__(self, method_name):
def __init__(self, method_name, sudo_pythonpath):
self.auto_nets = []
python_path = os.path.dirname(os.path.dirname(__file__))
argvbase = ([sys.executable, sys.argv[0]] +
['-v'] * (helpers.verbose or 0) +
['--method', method_name] +
['--firewall'])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [
['sudo', '-p', '[local sudo] Password: ', '/usr/bin/env',
('PYTHONPATH=%s' % python_path)] + 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.
# A list of commands that we can try to run to start the firewall.
argv_tries = []
if os.getuid() == 0: # No need to elevate privileges
argv_tries.append(argvbase)
else:
# Linux typically uses sudo; OpenBSD uses doas. However, some
# Linux distributions are starting to use doas.
sudo_cmd = ['sudo', '-p', '[local sudo] Password: ']+argvbase
doas_cmd = ['doas']+argvbase
# For clarity, try to replace executable name with the
# full path.
doas_path = which("doas")
if doas_path:
doas_cmd[0] = doas_path
sudo_path = which("sudo")
if sudo_path:
sudo_cmd[0] = sudo_path
# sudo_pythonpath indicates if we should set the
# PYTHONPATH environment variable when elevating
# privileges. This can be adjusted with the
# --no-sudo-pythonpath option.
if sudo_pythonpath:
pp_prefix = ['/usr/bin/env',
'PYTHONPATH=%s' %
os.path.dirname(os.path.dirname(__file__))]
sudo_cmd = pp_prefix + sudo_cmd
doas_cmd = pp_prefix + doas_cmd
# If we can find doas and not sudo or if we are on
# OpenBSD, try using doas first.
if (doas_path and not sudo_path) or \
platform.platform().startswith('OpenBSD'):
argv_tries = [doas_cmd, sudo_cmd, argvbase]
else:
argv_tries = [sudo_cmd, doas_cmd, argvbase]
# Try all commands in argv_tries in order. If a command
# produces an error, try the next one. If command is
# successful, set 'success' variable and break.
success = False
for argv in argv_tries:
# 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] ')
debug1("Starting firewall manager with command: %r" % argv)
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
e = None
break
# No env: Talking to `FirewallClient.start`, which has no i18n.
except OSError as e:
pass
# This exception will occur if the program isn't
# present or isn't executable.
debug1('Unable to start firewall manager. Popen failed. '
'Command=%r Exception=%s' % (argv, e))
continue
self.argv = argv
s1.close()
if sys.version_info < (3, 0):
# python 2.7
self.pfile = s2.makefile('wb+')
else:
# python 3.5
self.pfile = s2.makefile('rwb')
if e:
log('Spawning firewall manager: %r\n' % self.argv)
raise Fatal(e)
line = self.pfile.readline()
self.check()
rv = self.p.poll() # Check if process is still running
if rv:
# We might get here if program runs and exits before
# outputting anything. For example, someone might have
# entered the wrong password to elevate privileges.
debug1('Unable to start firewall manager. '
'Process exited too early. '
'%r returned %d' % (self.argv, rv))
continue
if line[0:5] != b'READY':
raise Fatal('%r expected READY, got %r' % (self.argv, line))
debug1('Unable to start firewall manager. '
'Expected READY, got %r. '
'Command=%r' % (line, self.argv))
continue
method_name = line[6:-1]
self.method = get_method(method_name.decode("ASCII"))
self.method.set_firewall(self)
success = True
break
if not success:
raise Fatal("All attempts to elevate privileges failed.")
def setup(self, subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
user):
user, tmark):
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
self.nslist = nslist
@ -250,6 +305,7 @@ class FirewallClient:
self.dnsport_v4 = dnsport_v4
self.udp = udp
self.user = user
self.tmark = tmark
def check(self):
rv = self.p.poll()
@ -260,11 +316,13 @@ class FirewallClient:
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))
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'%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:
@ -286,7 +344,8 @@ class FirewallClient:
else:
user = b'%d' % self.user
self.pfile.write(b'GO %d %s\n' % (udp, user))
self.pfile.write(b'GO %d %s %s %d\n' %
(udp, user, bytes(self.tmark, 'ascii'), os.getpid()))
self.pfile.flush()
line = self.pfile.readline()
@ -295,8 +354,8 @@ class FirewallClient:
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip):
assert(not re.search(b'[^-\w\.]', hostname))
assert(not re.search(b'[^0-9.]', ip))
assert(not re.search(br'[^-\w\.]', hostname))
assert(not re.search(br'[^0-9.]', ip))
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
self.pfile.flush()
@ -315,23 +374,23 @@ def expire_connections(now, mux):
remove = []
for chan, timeout in dnsreqs.items():
if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan)
debug3('expiring dnsreqs channel=%d' % chan)
remove.append(chan)
del mux.channels[chan]
for chan in remove:
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
debug3('Remaining DNS requests: %d' % 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))
debug3('expiring UDP channel channel=%d peer=%r' % (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))
debug3('Remaining UDP channels: %d' % len(udp_by_src))
def onaccept_tcp(listener, method, mux, handlers):
@ -340,28 +399,28 @@ def onaccept_tcp(listener, method, mux, handlers):
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')
debug1('Rejected incoming connection: too many open files!')
# 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)
_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],
debug1('Accept TCP: %s:%r -> %s:%r.' % (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")
debug1("-- ignored: that's my address!")
sock.close()
return
chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
log('warning: too many open channels. Discarded connection.')
sock.close()
return
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
@ -374,7 +433,7 @@ def onaccept_tcp(listener, method, mux, handlers):
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,))
debug3('doing send from %r to %r' % (srcip, dstip,))
method.send_udp(sock, srcip, dstip, data)
@ -384,7 +443,7 @@ def onaccept_udp(listener, method, mux, handlers):
if t is None:
return
srcip, dstip, data = t
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
debug1('Accept UDP: %r -> %r.' % (srcip, dstip,))
if srcip in udp_by_src:
chan, _ = udp_by_src[srcip]
else:
@ -401,7 +460,7 @@ def onaccept_udp(listener, method, mux, handlers):
def dns_done(chan, data, method, sock, srcip, dstip, mux):
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
debug3('dns_done: channel=%d src=%r dst=%r' % (chan, srcip, dstip))
del mux.channels[chan]
del dnsreqs[chan]
method.send_udp(sock, srcip, dstip, data)
@ -413,7 +472,13 @@ def ondns(listener, method, mux, handlers):
if t is None:
return
srcip, dstip, data = t
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
# dstip is None if we are using a method where we can't determine
# the destination IP of the DNS request that we captured from the client.
if dstip is None:
debug1('DNS request from %r: %d bytes' % (srcip, len(data)))
else:
debug1('DNS request from %r to %r: %d bytes' %
(srcip, dstip, len(data)))
chan = mux.next_channel()
dnsreqs[chan] = now + 30
mux.send(chan, ssnet.CMD_DNS_REQ, data)
@ -423,35 +488,34 @@ def ondns(listener, method, mux, handlers):
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control,
python, latency_control, latency_buffer_size,
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
to_nameserver):
debug1('Starting client with Python version %s\n'
helpers.logprefix = 'c : '
debug1('Starting client with Python version %s'
% platform.python_version())
method = fw.method
handlers = []
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
debug1('connecting to server...\n')
debug1('Connecting to server...')
try:
(serverproc, serversock) = ssh.connect(
ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control,
latency_buffer_size=latency_buffer_size,
auto_hosts=auto_hosts,
to_nameserver=to_nameserver))
to_nameserver=to_nameserver,
auto_nets=auto_nets))
except socket.error as e:
if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)")
else:
raise
mux = Mux(serversock, serversock)
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
handlers.append(mux)
expected = b'SSHUTTLE0001'
@ -470,33 +534,111 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
else:
raise
# Returns None if process is still running (or returns exit code)
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
if rv is not None:
errmsg = "server died with error code %d\n" % rv
# Our fatal exceptions return exit code 99
if rv == 99:
errmsg += "This error code likely means that python started and " \
"the sshuttle server started. However, the sshuttle server " \
"may have raised a 'Fatal' exception after it started."
elif rv == 98:
errmsg += "This error code likely means that we were able to " \
"run python on the server, but that the program continued " \
"to the line after we call python's exec() to execute " \
"sshuttle's server code. Try specifying the python " \
"executable to user on the server by passing --python " \
"to sshuttle."
# This error should only be possible when --python is not specified.
elif rv == 97 and not python:
errmsg += "This error code likely means that either we " \
"couldn't find python3 or python in the PATH on the " \
"server or that we do not have permission to run 'exec' in " \
"the /bin/sh shell on the server. Try specifying the " \
"python executable to use on the server by passing " \
"--python to sshuttle."
# POSIX sh standards says error code 127 is used when you try
# to execute a program that does not exist. See section 2.8.2
# of
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08
elif rv == 127:
if python:
errmsg += "This error code likely means that we were not " \
"able to execute the python executable that specified " \
"with --python. You specified '%s'.\n" % python
if python.startswith("/"):
errmsg += "\nTip for users in a restricted shell on the " \
"server: The server may refuse to run programs " \
"specified with an absolute path. Try specifying " \
"just the name of the python executable. However, " \
"if python is not in your PATH and you cannot " \
"run programs specified with an absolute path, " \
"it is possible that sshuttle will not work."
else:
errmsg += "This error code likely means that we were unable " \
"to execute /bin/sh on the remote server. This can " \
"happen if /bin/sh does not exist on the server or if " \
"you are in a restricted shell that does not allow you " \
"to run programs specified with an absolute path. " \
"Try rerunning sshuttle with the --python parameter."
# When the redirected subnet includes the remote ssh host, the
# firewall rules can interrupt the ssh connection to the
# remote machine. This issue impacts some Linux machines. The
# user sees that the server dies with a broken pipe error and
# code 255.
#
# The solution to this problem is to exclude the remote
# server.
#
# There are many github issues from users encountering this
# problem. Most of the discussion on the topic is here:
# https://github.com/sshuttle/sshuttle/issues/191
elif rv == 255:
errmsg += "It might be possible to resolve this error by " \
"excluding the server that you are ssh'ing to. For example, " \
"if you are running 'sshuttle -v -r example.com 0/0' to " \
"redirect all traffic through example.com, then try " \
"'sshuttle -v -r example.com -x example.com 0/0' to " \
"exclude redirecting the connection to example.com itself " \
"(i.e., sshuttle's firewall rules may be breaking the " \
"ssh connection that it previously established). " \
"Alternatively, you may be able to use 'sshuttle -v -r " \
"example.com -x example.com:22 0/0' to redirect " \
"everything except ssh connections between your machine " \
"and example.com."
raise Fatal(errmsg)
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
log('Connected.\n')
log('Connected to server.')
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
log('daemonizing (%s).' % _pidname)
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split(b'\n'):
if not line: continue
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))
debug2("Ignored auto net %d/%s/%d" % (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))
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
else:
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width))
debug2("Adding auto net %d/%s/%d" % (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
@ -507,11 +649,16 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
# set --auto-nets, we might as well wait for the message first, then
# ignore its contents.
mux.got_routes = None
fw.start()
serverready()
mux.got_routes = onroutes
def serverready():
fw.start()
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist)
debug2('got host list: %r' % hostlist)
for line in hostlist.strip().split():
if line:
name, ip = line.split(b',', 1)
@ -527,60 +674,132 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
dns_listener.add_handler(handlers, ondns, method, mux)
if seed_hosts is not None:
debug1('seed_hosts: %r\n' % seed_hosts)
debug1('seed_hosts: %r' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
while 1:
def check_ssh_alive():
if daemon:
# poll() won't tell us when process exited since the
# process is no longer our child (it returns 0 all the
# time).
try:
os.kill(serverproc.pid, 0)
except OSError:
raise Fatal('ssh connection to server (pid %d) exited.' %
serverproc.pid)
else:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
# poll returns None if process hasn't exited.
if rv is not None:
raise Fatal('ssh connection to server (pid %d) exited '
'with returncode %d' % (serverproc.pid, rv))
while 1:
check_ssh_alive()
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, nslist,
ssh_cmd, remotename, python, latency_control,
latency_buffer_size, dns, nslist,
method_name, seed_hosts, auto_hosts, auto_nets,
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
user):
user, sudo_pythonpath, tmark):
if not remotename:
raise Fatal("You must use -r/--remote to specify a remote "
"host to route traffic through.")
if daemon:
try:
check_daemon(pidfile)
except Fatal as e:
log("%s\n" % e)
log("%s" % e)
return 5
debug1('Starting sshuttle proxy.\n')
debug1('Starting sshuttle proxy (version %s).' % __version__)
helpers.logprefix = 'c : '
fw = FirewallClient(method_name)
fw = FirewallClient(method_name, sudo_pythonpath)
# Get family specific subnet lists
# nslist is the list of name severs to intercept. If --dns is
# used, we add all DNS servers in resolv.conf. Otherwise, the list
# can be populated with the --ns-hosts option (which is already
# stored in nslist). This list is used to setup the firewall so it
# can redirect packets outgoing to this server to the remote host
# instead.
if dns:
nslist += resolvconf_nameservers()
nslist += resolvconf_nameservers(True)
# If we are intercepting DNS requests, we tell the remote host
# where it should send the DNS requests to with the --to-ns
# option.
if len(nslist) > 0:
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
else: # if we are not intercepting DNS traffic
# ...and the user specified a server to send DNS traffic to.
if to_nameserver and len(to_nameserver) > 0:
print("WARNING: --to-ns option is ignored unless "
"--dns or --ns-hosts is used.")
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]
# Get family specific subnet lists. Also, the user may not specify
# any subnets if they use --auto-nets. In this case, our subnets
# list will be empty and the forwarded subnets will be determined
# later by the server.
subnets_v4 = [i for i in subnets_include if i[0] == socket.AF_INET]
subnets_v6 = [i for i in subnets_include if i[0] == socket.AF_INET6]
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
# Check features available
# Get available features from the firewall method
avail = fw.method.get_supported_features()
# A feature is "required" if the user supplies us parameters which
# implies that the feature is needed.
required = Features()
# Select the default addresses to bind to / listen to.
# Assume IPv4 is always available and should always be enabled. If
# a method doesn't provide IPv4 support or if we wish to run
# ipv6-only, changes to this code are required.
assert avail.ipv4
required.ipv4 = True
# listenip_v4 contains user specified value or it is set to "auto".
if listenip_v4 == "auto":
listenip_v4 = ('127.0.0.1', 0)
# listenip_v6 is...
# None when IPv6 is disabled.
# "auto" when listen address is unspecified.
# The user specified address if provided by user
if listenip_v6 is None:
debug1("IPv6 disabled by --disable-ipv6")
if listenip_v6 == "auto":
if avail.ipv6:
debug1("IPv6 enabled: Using default IPv6 listen address ::1")
listenip_v6 = ('::1', 0)
else:
debug1("IPv6 disabled since it isn't supported by method "
"%s." % fw.method.name)
listenip_v6 = None
# Make final decision about enabling IPv6:
required.ipv6 = False
if listenip_v6:
required.ipv6 = True
# If we get here, it is possible that listenip_v6 was user
# specified but not supported by the current method.
if required.ipv6 and not avail.ipv6:
raise Fatal("An IPv6 listen address was supplied, but IPv6 is "
"disabled at your request or is unsupported by the %s "
"method." % fw.method.name)
if user is not None:
if getpwnam is None:
raise Fatal("Routing by user not available on this system.")
@ -588,33 +807,66 @@ def main(listenip_v6, listenip_v4,
user = getpwnam(user).pw_uid
except KeyError:
raise Fatal("User %s does not exist." % user)
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
required.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:
if not required.ipv6 and len(subnets_v6) > 0:
print("WARNING: IPv6 subnets were ignored because IPv6 is disabled "
"in sshuttle.")
subnets_v6 = []
subnets_include = subnets_v4
required.udp = avail.udp # automatically enable UDP if it is available
required.dns = len(nslist) > 0
# Remove DNS servers using IPv6.
if required.dns:
if not required.ipv6 and len(nslist_v6) > 0:
print("WARNING: Your system is configured to use an IPv6 DNS "
"server but sshuttle is not using IPv6. Therefore DNS "
"traffic your system sends to the IPv6 DNS server won't "
"be redirected via sshuttle to the remote machine.")
nslist_v6 = []
nslist = nslist_v4
if len(nslist) == 0:
raise Fatal("Can't redirect DNS traffic since IPv6 is not "
"enabled in sshuttle and all of the system DNS "
"servers are IPv6.")
# If we aren't using IPv6, we can safely ignore excluded IPv6 subnets.
if not required.ipv6:
orig_len = len(subnets_exclude)
subnets_exclude = [i for i in subnets_exclude
if i[0] == socket.AF_INET]
if len(subnets_exclude) < orig_len:
print("WARNING: Ignoring one or more excluded IPv6 subnets "
"because IPv6 is not enabled.")
# This will print error messages if we required a feature that
# isn't available by the current method.
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)
def feature_status(label, enabled, available):
msg = label + ": "
if enabled:
msg += "on"
else:
msg += "off "
if available:
msg += "(available)"
else:
msg += "(not available with %s method)" % fw.method.name
debug1(msg)
# bind to required ports
if listenip_v4 == "auto":
listenip_v4 = ('127.0.0.1', 0)
debug1("Method: %s" % fw.method.name)
feature_status("IPv4", required.ipv4, avail.ipv4)
feature_status("IPv6", required.ipv6, avail.ipv6)
feature_status("UDP ", required.udp, avail.udp)
feature_status("DNS ", required.dns, avail.dns)
feature_status("User", required.user, avail.user)
# Exclude traffic destined to our listen addresses.
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))
@ -623,6 +875,25 @@ def main(listenip_v6, listenip_v4,
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
# We don't print the IP+port of where we are listening here
# because we do that below when we have identified the ports to
# listen on.
debug1("Subnets to forward through remote host (type, IP, cidr mask "
"width, startPort, endPort):")
for i in subnets_include:
debug1(" "+str(i))
if auto_nets:
debug1("NOTE: Additional subnets to forward may be added below by "
"--auto-nets.")
debug1("Subnets to exclude from forwarding:")
for i in subnets_exclude:
debug1(" "+str(i))
if required.dns:
debug1("DNS requests normally directed at these servers will be "
"redirected to remote:")
for i in nslist:
debug1(" "+str(i))
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, ]
@ -638,9 +909,8 @@ def main(listenip_v6, listenip_v4,
redirectport_v6 = 0
redirectport_v4 = 0
bound = False
debug2('Binding redirector:')
for port in ports:
debug2(' %d' % port)
debug2('Trying to bind redirector on port %d' % port)
tcp_listener = MultiListener()
if required.udp:
@ -682,7 +952,6 @@ def main(listenip_v6, listenip_v4,
else:
raise e
debug2('\n')
if not bound:
assert(last_e)
raise last_e
@ -694,11 +963,11 @@ def main(listenip_v6, listenip_v4,
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
debug2('Trying to bind DNS redirector on port %d' % port)
if port in used_ports:
continue
dns_listener = MultiListener(socket.SOCK_DGRAM)
@ -727,7 +996,7 @@ def main(listenip_v6, listenip_v4,
used_ports.append(port)
else:
raise e
debug2('\n')
dns_listener.print_listening("DNS")
if not bound:
assert(last_e)
@ -769,19 +1038,22 @@ def main(listenip_v6, listenip_v4,
# start the firewall
fw.setup(subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
required.udp, user)
required.udp, user, tmark)
# 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)
python, latency_control, latency_buffer_size,
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()

View File

@ -1,5 +1,6 @@
import re
import socket
import platform
import sshuttle.helpers as helpers
import sshuttle.client as client
import sshuttle.firewall as firewall
@ -7,16 +8,35 @@ 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')
return 1
if not opt.sudoers_filename:
log('--sudoers-file must be set or omitted.')
return 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:
@ -27,8 +47,16 @@ def main():
elif opt.hostwatch:
return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
else:
includes = opt.subnets + opt.subnets_file
excludes = opt.exclude
# parse_subnetports() is used to create a list of includes
# and excludes. It is called once for each parameter and
# returns a list of one or more items for each subnet (it
# can return more than one item when a hostname in the
# parameter resolves to multiple IP addresses. Here, we
# flatten these lists.
includes = [item for sublist in opt.subnets+opt.subnets_file
for item in sublist]
excludes = [item for sublist in opt.exclude for item in sublist]
if not includes and not opt.auto_nets:
parser.error('at least one subnet, subnet file, '
'or -N expected')
@ -57,14 +85,24 @@ def main():
ipport_v4 = "auto"
# parse_ipport6('[::1]:0')
ipport_v6 = "auto" if not opt.disable_ipv6 else None
try:
int(opt.tmark, 16)
except ValueError:
parser.error("--tmark must be a hexadecimal value")
opt.tmark = opt.tmark.lower() # make 'x' in 0x lowercase
if not opt.tmark.startswith("0x"): # accept without 0x prefix
opt.tmark = "0x%s" % opt.tmark
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.latency_buffer_size,
opt.dns,
nslist,
opt.method,
@ -76,18 +114,20 @@ def main():
opt.daemon,
opt.to_ns,
opt.pidfile,
opt.user)
opt.user,
opt.sudo_pythonpath,
opt.tmark)
if return_code == 0:
log('Normal exit code, exiting...')
else:
log('Abnormal exit code detected, failing...' % return_code)
log('Abnormal exit code %d detected, failing...' % return_code)
return return_code
except Fatal as e:
log('fatal: %s\n' % e)
log('fatal: %s' % e)
return 99
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
log('Keyboard interrupt: exiting.')
return 1

View File

@ -1,16 +1,19 @@
import errno
import socket
import signal
import sshuttle.ssyslog as ssyslog
import sshuttle.sdnotify as sdnotify
import sys
import os
import platform
import traceback
from sshuttle.helpers import debug1, debug2, Fatal
import subprocess as ssubprocess
import sshuttle.ssyslog as ssyslog
import sshuttle.helpers as helpers
from sshuttle.helpers import log, debug1, debug2, Fatal
from sshuttle.methods import get_auto_method, get_method
HOSTSFILE = '/etc/hosts'
sshuttle_pid = None
def rewrite_etc_hosts(hostmap, port):
@ -47,25 +50,54 @@ def rewrite_etc_hosts(hostmap, port):
os.rename(tmpname, HOSTSFILE)
def restore_etc_hosts(port):
def restore_etc_hosts(hostmap, port):
# Only restore if we added hosts to /etc/hosts previously.
if len(hostmap) > 0:
debug2('undoing /etc/hosts changes.')
rewrite_etc_hosts({}, port)
def firewall_exit(signum, frame):
# The typical sshuttle exit is that the main sshuttle process
# exits, closes file descriptors it uses, and the firewall process
# notices that it can't read from stdin anymore and exits
# (cleaning up firewall rules).
#
# However, in some cases, Ctrl+C might get sent to the firewall
# process. This might caused if someone manually tries to kill the
# firewall process, or if sshuttle was started using sudo's use_pty option
# and they try to exit by pressing Ctrl+C. Here, we forward the
# Ctrl+C/SIGINT to the main sshuttle process which should trigger
# the typical exit process as described above.
global sshuttle_pid
if sshuttle_pid:
debug1("Relaying SIGINT to sshuttle process %d\n" % sshuttle_pid)
os.kill(sshuttle_pid, signal.SIGINT)
# 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')
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)
signal.signal(signal.SIGTERM, firewall_exit)
signal.signal(signal.SIGINT, firewall_exit)
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
# I'll die automatically.
# Calling setsid() here isn't strictly necessary. However, it forces
# Ctrl+C to get sent to the main sshuttle process instead of to
# the firewall process---which is our preferred way to shutdown.
# Nonetheless, if the firewall process receives a SIGTERM/SIGINT
# signal, it will relay a SIGINT to the main sshuttle process
# automatically.
try:
os.setsid()
except OSError:
# setsid() fails if sudo is configured with the use_pty option.
pass
# because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we
@ -85,19 +117,50 @@ def subnet_weight(s):
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
def flush_systemd_dns_cache():
# If the user is using systemd-resolve for DNS resolution, it is
# possible for the request to go through systemd-resolve before we
# see it...and it may use a cached result instead of sending a
# request that we can intercept. When sshuttle starts and stops,
# this means that we should clear the cache!
#
# The command to do this was named systemd-resolve, but changed to
# resolvectl in systemd 239.
# https://github.com/systemd/systemd/blob/f8eb41003df1a4eab59ff9bec67b2787c9368dbd/NEWS#L3816
p = None
if helpers.which("resolvectl"):
debug2("Flushing systemd's DNS resolver cache: "
"resolvectl flush-caches")
p = ssubprocess.Popen(["resolvectl", "flush-caches"],
stdout=ssubprocess.PIPE, env=helpers.get_env())
elif helpers.which("systemd-resolve"):
debug2("Flushing systemd's DNS resolver cache: "
"systemd-resolve --flush-caches")
p = ssubprocess.Popen(["systemd-resolve", "--flush-caches"],
stdout=ssubprocess.PIPE, env=helpers.get_env())
if p:
# Wait so flush is finished and process doesn't show up as defunct.
rv = p.wait()
if rv != 0:
log("Received non-zero return code %d when flushing DNS resolver "
"cache." % rv)
# 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
# supersede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless.
def main(method_name, syslog):
helpers.logprefix = 'fw: '
stdin, stdout = setup_daemon()
hostmap = {}
debug1('firewall manager: Starting firewall with Python version %s\n'
debug1('Starting firewall with Python version %s'
% platform.python_version())
if method_name == "auto":
@ -109,7 +172,12 @@ def main(method_name, syslog):
ssyslog.start_syslog()
ssyslog.stderr_to_syslog()
debug1('firewall manager: ready method name %s.\n' % method.name)
if not method.is_supported():
raise Fatal("The %s method is not supported on this machine. "
"Check that the appropriate programs are in your "
"PATH." % method_name)
debug1('ready method name %s.' % method.name)
stdout.write('READY %s\n' % method.name)
stdout.flush()
@ -122,18 +190,18 @@ def main(method_name, syslog):
subnets = []
if line != 'ROUTES\n':
raise Fatal('firewall: expected ROUTES but got %r' % line)
raise Fatal('expected ROUTES but got %r' % line)
while 1:
line = stdin.readline(128)
if not line:
raise Fatal('firewall: expected route but got %r' % line)
raise Fatal('expected route but got %r' % line)
elif line.startswith("NSLIST\n"):
break
try:
(family, width, exclude, ip, fport, lport) = \
line.strip().split(',', 5)
except:
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
except BaseException:
raise Fatal('expected route or NSLIST but got %r' % line)
subnets.append((
int(family),
int(width),
@ -141,31 +209,31 @@ def main(method_name, syslog):
ip,
int(fport),
int(lport)))
debug2('firewall manager: Got subnets: %r\n' % subnets)
debug2('Got subnets: %r' % subnets)
nslist = []
if line != 'NSLIST\n':
raise Fatal('firewall: expected NSLIST but got %r' % line)
raise Fatal('expected NSLIST but got %r' % line)
while 1:
line = stdin.readline(128)
if not line:
raise Fatal('firewall: expected nslist but got %r' % line)
raise Fatal('expected nslist but got %r' % line)
elif line.startswith("PORTS "):
break
try:
(family, ip) = line.strip().split(',', 1)
except:
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
except BaseException:
raise Fatal('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)
debug2('Got partial nslist: %r' % nslist)
debug2('Got nslist: %r' % nslist)
if not line.startswith('PORTS '):
raise Fatal('firewall: expected PORTS but got %r' % line)
raise Fatal('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))
raise Fatal('expected 4 ports but got %d' % len(ports))
port_v6 = int(ports[0])
port_v4 = int(ports[1])
dnsport_v6 = int(ports[2])
@ -180,21 +248,24 @@ def main(method_name, syslog):
assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535)
debug2('firewall manager: Got ports: %d,%d,%d,%d\n'
debug2('Got ports: %d,%d,%d,%d'
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
line = stdin.readline(128)
if not line:
raise Fatal('firewall: expected GO but got %r' % line)
raise Fatal('expected GO but got %r' % line)
elif not line.startswith("GO "):
raise Fatal('firewall: expected GO but got %r' % line)
raise Fatal('expected GO but got %r' % line)
_, _, args = line.partition(" ")
udp, user = args.strip().split(" ", 1)
global sshuttle_pid
udp, user, tmark, sshuttle_pid = args.strip().split(" ", 3)
udp = bool(int(udp))
sshuttle_pid = int(sshuttle_pid)
if user == '-':
user = None
debug2('firewall manager: Got udp: %r, user: %r\n' % (udp, user))
debug2('Got udp: %r, user: %r, tmark: %s, sshuttle_pid: %d' %
(udp, user, tmark, sshuttle_pid))
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]
@ -202,25 +273,24 @@ def main(method_name, syslog):
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
try:
debug1('firewall manager: setting up.\n')
debug1('setting up.')
if subnets_v6 or nslist_v6:
debug2('firewall manager: setting up IPv6.\n')
debug2('setting up IPv6.')
method.setup_firewall(
port_v6, dnsport_v6, nslist_v6,
socket.AF_INET6, subnets_v6, udp,
user)
user, tmark)
if subnets_v4 or nslist_v4:
debug2('firewall manager: setting up IPv4.\n')
debug2('setting up IPv4.')
method.setup_firewall(
port_v4, dnsport_v4, nslist_v4,
socket.AF_INET, subnets_v4, udp,
user)
user, tmark)
flush_systemd_dns_cache()
stdout.write('STARTED\n')
sdnotify.send(sdnotify.ready(),
sdnotify.status('Connected'))
try:
stdout.flush()
@ -237,54 +307,56 @@ def main(method_name, syslog):
if line.startswith('HOST '):
(name, ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip
debug2('firewall manager: setting up /etc/hosts.\n')
debug2('setting up /etc/hosts.')
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)
raise Fatal('expected command, got %r' % line)
else:
break
finally:
try:
sdnotify.send(sdnotify.stop())
debug1('firewall manager: undoing changes.\n')
except:
pass
debug1('undoing changes.')
except BaseException:
debug2('An error occurred, ignoring it.')
try:
if subnets_v6 or nslist_v6:
debug2('firewall manager: undoing IPv6 changes.\n')
debug2('undoing IPv6 changes.')
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
except:
except BaseException:
try:
debug1("firewall manager: "
"Error trying to undo IPv6 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("---> %s\n" % line)
except:
pass
debug1("Error trying to undo IPv6 firewall.")
debug1(traceback.format_exc())
except BaseException:
debug2('An error occurred, ignoring it.')
try:
if subnets_v4 or nslist_v4:
debug2('firewall manager: undoing IPv4 changes.\n')
debug2('undoing IPv4 changes.')
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
except:
except BaseException:
try:
debug1("firewall manager: "
"Error trying to undo IPv4 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("firewall manager: ---> %s\n" % line)
except:
pass
debug1("Error trying to undo IPv4 firewall.")
debug1(traceback.format_exc())
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:
# debug2() message printed in restore_etc_hosts() function.
restore_etc_hosts(hostmap, 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:
pass
debug1("Error trying to undo /etc/hosts changes.")
debug1(traceback.format_exc())
except BaseException:
debug2('An error occurred, ignoring it.')
try:
flush_systemd_dns_cache()
except BaseException:
try:
debug1("Error trying to flush systemd dns cache.")
debug1(traceback.format_exc())
except BaseException:
debug2("An error occurred, ignoring it.")

View File

@ -1,34 +1,36 @@
import sys
import socket
import errno
import os
logprefix = ''
verbose = 0
if sys.version_info[0] == 3:
binary_type = bytes
def b(s):
return s.encode("ASCII")
else:
binary_type = str
def b(s):
return s
def log(s):
global logprefix
try:
sys.stdout.flush()
if s.find("\n") != -1:
# Put newline at end of string if line doesn't have one.
if not s.endswith("\n"):
s = s+"\n"
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)
# We output with \r\n instead of \n because when we use
# sudo with the use_pty option, the firewall process, the
# other processes printing to the terminal will have the
# \n move to the next line, but they will fail to reset
# cursor to the beginning of the line. Printing output
# with \r\n endings fixes that problem and does not appear
# to cause problems elsewhere.
sys.stderr.write(prefix + line + "\r\n")
prefix = " "
sys.stderr.flush()
except IOError:
# this could happen if stderr gets forcibly disconnected, eg. because
@ -55,23 +57,70 @@ class Fatal(Exception):
pass
def resolvconf_nameservers():
l = []
for line in open('/etc/resolv.conf'):
def resolvconf_nameservers(systemd_resolved):
"""Retrieves a list of tuples (address type, address as a string) of
the DNS servers used by the system to resolve hostnames.
If parameter is False, DNS servers are retrieved from only
/etc/resolv.conf. This behavior makes sense for the sshuttle
server.
If parameter is True, we retrieve information from both
/etc/resolv.conf and /run/systemd/resolve/resolv.conf (if it
exists). This behavior makes sense for the sshuttle client.
"""
# Historically, we just needed to read /etc/resolv.conf.
#
# If systemd-resolved is active, /etc/resolv.conf will point to
# localhost and the actual DNS servers that systemd-resolved uses
# are stored in /run/systemd/resolve/resolv.conf. For programs
# that use the localhost DNS server, having sshuttle read
# /etc/resolv.conf is sufficient. However, resolved provides other
# ways of resolving hostnames (such as via dbus) that may not
# route requests through localhost. So, we retrieve a list of DNS
# servers that resolved uses so we can intercept those as well.
#
# For more information about systemd-resolved, see:
# https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html
#
# On machines without systemd-resolved, we expect opening the
# second file will fail.
files = ['/etc/resolv.conf']
if systemd_resolved:
files += ['/run/systemd/resolve/resolv.conf']
nsservers = []
for f in files:
this_file_nsservers = []
try:
for line in open(f):
words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver':
l.append(family_ip_tuple(words[1]))
return l
this_file_nsservers.append(family_ip_tuple(words[1]))
debug2("Found DNS servers in %s: %s" %
(f, [n[1] for n in this_file_nsservers]))
nsservers += this_file_nsservers
except OSError as e:
debug3("Failed to read %s when looking for DNS servers: %s" %
(f, e.strerror))
return nsservers
def resolvconf_random_nameserver():
l = resolvconf_nameservers()
if l:
if len(l) > 1:
def resolvconf_random_nameserver(systemd_resolved):
"""Return a random nameserver selected from servers produced by
resolvconf_nameservers(). See documentation for
resolvconf_nameservers() for a description of the parameter.
"""
lines = resolvconf_nameservers(systemd_resolved)
if lines:
if len(lines) > 1:
# don't import this unless we really need it
import random
random.shuffle(l)
return l[0]
random.shuffle(lines)
return lines[0]
else:
return (socket.AF_INET, '127.0.0.1')
@ -106,3 +155,75 @@ def family_to_string(family):
return "AF_INET"
else:
return str(family)
def get_env():
"""An environment for sshuttle subprocesses. See get_path()."""
env = {
'PATH': get_path(),
'LC_ALL': "C",
}
return env
def get_path():
"""Returns a string of paths separated by os.pathsep.
Users might not have all of the programs sshuttle needs in their
PATH variable (i.e., some programs might be in /sbin). Use PATH
and a hardcoded set of paths to search through. This function is
used by our which() and get_env() functions. If which() and the
subprocess environments differ, programs that which() finds might
not be found at run time (or vice versa).
"""
path = []
if "PATH" in os.environ:
path += os.environ["PATH"].split(os.pathsep)
# Python default paths.
path += os.defpath.split(os.pathsep)
# /sbin, etc are not in os.defpath and may not be in PATH either.
# /bin/ and /usr/bin below are probably redundant.
path += ['/bin', '/usr/bin', '/sbin', '/usr/sbin']
# Remove duplicates. Not strictly necessary.
path_dedup = []
for i in path:
if i not in path_dedup:
path_dedup.append(i)
return os.pathsep.join(path_dedup)
if sys.version_info >= (3, 3):
from shutil import which as _which
else:
# Although sshuttle does not officially support older versions of
# Python, some still run the sshuttle server on remote machines
# with old versions of python.
def _which(file, mode=os.F_OK | os.X_OK, path=None):
if path is not None:
search_paths = path.split(os.pathsep)
elif "PATH" in os.environ:
search_paths = os.environ["PATH"].split(os.pathsep)
else:
search_paths = os.defpath.split(os.pathsep)
for p in search_paths:
filepath = os.path.join(p, file)
if os.path.exists(filepath) and os.access(filepath, mode):
return filepath
return None
def which(file, mode=os.F_OK | os.X_OK):
"""A wrapper around shutil.which() that searches a predictable set of
paths and is more verbose about what is happening. See get_path()
for more information.
"""
path = get_path()
rv = _which(file, mode, path)
if rv:
debug2("which() found '%s' at %s" % (file, rv))
else:
debug2("which() could not find '%s' in %s" % (file, path))
return rv

View File

@ -9,22 +9,22 @@ import platform
import subprocess as ssubprocess
import sshuttle.helpers as helpers
from sshuttle.helpers import log, debug1, debug2, debug3
from sshuttle.helpers import log, debug1, debug2, debug3, get_env
POLL_TIME = 60 * 15
NETSTAT_POLL_TIME = 30
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
# Have we already failed to write CACHEFILE?
CACHE_WRITE_FAILED = False
_nmb_ok = True
_smb_ok = True
hostnames = {}
queue = {}
try:
null = open('/dev/null', 'wb')
null = open(os.devnull, 'wb')
except IOError:
_, e = sys.exc_info()[:2]
log('warning: %s\n' % e)
log('warning: %s' % e)
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
@ -33,7 +33,10 @@ def _is_ip(s):
def write_host_cache():
"""If possible, write our hosts file to disk so future connections
can reuse the hosts that we already found."""
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
global CACHE_WRITE_FAILED
try:
f = open(tmpname, 'wb')
for name, ip in sorted(hostnames.items()):
@ -41,33 +44,50 @@ def write_host_cache():
f.close()
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
os.rename(tmpname, CACHEFILE)
finally:
CACHE_WRITE_FAILED = False
except (OSError, IOError):
# Write message if we haven't yet or if we get a failure after
# a previous success.
if not CACHE_WRITE_FAILED:
log("Failed to write host cache to temporary file "
"%s and rename it to %s" % (tmpname, CACHEFILE))
CACHE_WRITE_FAILED = True
try:
os.unlink(tmpname)
except:
except BaseException:
pass
def read_host_cache():
"""If possible, read the cache file from disk to populate hosts that
were found in a previous sshuttle run."""
try:
f = open(CACHEFILE)
except IOError:
except (OSError, IOError):
_, e = sys.exc_info()[:2]
if e.errno == errno.ENOENT:
return
else:
raise
log("Failed to read existing host cache file %s on remote host"
% CACHEFILE)
return
for line in f:
words = line.strip().split(',')
if len(words) == 2:
(name, ip) = words
name = re.sub(r'[^-\w\.]', '-', name).strip()
# Remove characters that shouldn't be in IP
ip = re.sub(r'[^0-9.]', '', ip).strip()
if name and ip:
found_host(name, ip)
def found_host(name, ip):
"""The provided name maps to the given IP. Add the host to the
hostnames list, send the host to the sshuttle client via
stdout, and write the host to the cache file.
"""
hostname = re.sub(r'\..*', '', name)
hostname = re.sub(r'[^-\w\.]', '_', hostname)
if (ip.startswith('127.') or ip.startswith('255.') or
@ -80,168 +100,87 @@ def found_host(name, ip):
oldip = hostnames.get(name)
if oldip != ip:
hostnames[name] = ip
debug1('Found: %s: %s\n' % (name, ip))
debug1('Found: %s: %s' % (name, ip))
sys.stdout.write('%s,%s\n' % (name, ip))
write_host_cache()
def _check_etc_hosts():
debug2(' > hosts\n')
for line in open('/etc/hosts'):
line = re.sub(r'#.*', '', line)
"""If possible, read /etc/hosts to find hosts."""
filename = '/etc/hosts'
debug2(' > Reading %s on remote host' % filename)
try:
for line in open(filename):
line = re.sub(r'#.*', '', line) # remove comments
words = line.strip().split()
if not words:
continue
ip = words[0]
names = words[1:]
if _is_ip(ip):
debug3('< %s %r\n' % (ip, names))
names = words[1:]
debug3('< %s %r' % (ip, names))
for n in names:
check_host(n)
found_host(n, ip)
except (OSError, IOError):
debug1("Failed to read %s on remote host" % filename)
def _check_revdns(ip):
debug2(' > rev: %s\n' % ip)
"""Use reverse DNS to try to get hostnames from an IP addresses."""
debug2(' > rev: %s' % ip)
try:
r = socket.gethostbyaddr(ip)
debug3('< %s\n' % r[0])
debug3('< %s' % r[0])
check_host(r[0])
found_host(r[0], ip)
except socket.herror:
except (OSError, socket.error, UnicodeError):
# This case is expected to occur regularly.
# debug3('< %s gethostbyaddr failed on remote host' % ip)
pass
def _check_dns(hostname):
debug2(' > dns: %s\n' % hostname)
debug2(' > dns: %s' % hostname)
try:
ip = socket.gethostbyname(hostname)
debug3('< %s\n' % ip)
debug3('< %s' % ip)
check_host(ip)
found_host(hostname, ip)
except socket.gaierror:
except (socket.gaierror, UnicodeError):
pass
def _check_netstat():
debug2(' > netstat\n')
debug2(' > netstat')
argv = ['netstat', '-n']
try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=get_env())
content = p.stdout.read().decode("ASCII")
p.wait()
except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e))
log('%r failed: %r' % (argv, e))
return
# The same IPs may appear multiple times. Consolidate them so the
# debug message doesn't print the same IP repeatedly.
ip_list = []
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
debug3('< %s\n' % ip)
if ip not in ip_list:
ip_list.append(ip)
for ip in sorted(ip_list):
debug3('< %s' % ip)
check_host(ip)
def _check_smb(hostname):
return
global _smb_ok
if not _smb_ok:
return
argv = ['smbclient', '-U', '%', '-L', hostname]
debug2(' > smb: %s\n' % hostname)
try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
lines = p.stdout.readlines()
p.wait()
except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e))
_smb_ok = False
return
lines.reverse()
# junk at top
while lines:
line = lines.pop().strip()
if re.match(r'Server\s+', line):
break
# server list section:
# Server Comment
# ------ -------
while lines:
line = lines.pop().strip()
if not line or re.match(r'-+\s+-+', line):
continue
if re.match(r'Workgroup\s+Master', line):
break
words = line.split()
hostname = words[0].lower()
debug3('< %s\n' % hostname)
check_host(hostname)
# workgroup list section:
# Workgroup Master
# --------- ------
while lines:
line = lines.pop().strip()
if re.match(r'-+\s+', line):
continue
if not line:
break
words = line.split()
(workgroup, hostname) = (words[0].lower(), words[1].lower())
debug3('< group(%s) -> %s\n' % (workgroup, hostname))
check_host(hostname)
check_workgroup(workgroup)
if lines:
assert(0)
def _check_nmb(hostname, is_workgroup, is_master):
return
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))
try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
lines = p.stdout.readlines()
rv = p.wait()
except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e))
_nmb_ok = False
return
if rv:
log('%r returned %d\n' % (argv, rv))
return
for line in lines:
m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line)
if m:
g = m.groups()
(ip, name) = (g[0], g[1].lower())
debug3('< %s -> %s\n' % (name, ip))
if is_workgroup:
_enqueue(_check_smb, ip)
else:
found_host(name, ip)
check_host(name)
def check_host(hostname):
if _is_ip(hostname):
_enqueue(_check_revdns, hostname)
else:
_enqueue(_check_dns, hostname)
_enqueue(_check_smb, hostname)
_enqueue(_check_nmb, hostname, False, False)
def check_workgroup(hostname):
_enqueue(_check_nmb, hostname, True, False)
_enqueue(_check_nmb, hostname, True, True)
def _enqueue(op, *args):
@ -260,12 +199,9 @@ def _stdin_still_ok(timeout):
def hw_main(seed_hosts, auto_hosts):
if helpers.verbose >= 2:
helpers.logprefix = 'HH: '
else:
helpers.logprefix = 'hostwatch: '
debug1('Starting hostwatch with Python version %s\n'
debug1('Starting hostwatch with Python version %s'
% platform.python_version())
for h in seed_hosts:
@ -277,18 +213,22 @@ def hw_main(seed_hosts, auto_hosts):
_enqueue(_check_netstat)
check_host('localhost')
check_host(socket.gethostname())
check_workgroup('workgroup')
check_workgroup('-')
while 1:
now = time.time()
# For each item in the queue
for t, last_polled in list(queue.items()):
(op, args) = t
if not _stdin_still_ok(0):
break
# Determine if we need to run.
maxtime = POLL_TIME
# netstat runs more often than other jobs
if op == _check_netstat:
maxtime = NETSTAT_POLL_TIME
# Check if this jobs needs to run.
if now - last_polled > maxtime:
queue[t] = time.time()
op(*args)
@ -298,5 +238,5 @@ def hw_main(seed_hosts, auto_hosts):
break
# FIXME: use a smarter timeout based on oldest last_polled
if not _stdin_still_ok(1):
if not _stdin_still_ok(1): # sleeps for up to 1 second
break

View File

@ -1,15 +1,13 @@
import re
import os
import socket
import subprocess as ssubprocess
from sshuttle.helpers import log, debug1, Fatal, family_to_string
from sshuttle.helpers import log, debug1, Fatal, family_to_string, get_env
def nonfatal(func, *args):
try:
func(*args)
except Fatal as e:
log('error: %s\n' % e)
log('error: %s' % e)
def ipt_chain_exists(family, table, name):
@ -19,87 +17,35 @@ def ipt_chain_exists(family, table, name):
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",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
for line in p.stdout:
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
argv = [cmd, '-w', '-t', table, '-nL']
try:
output = ssubprocess.check_output(argv, env=get_env())
for line in output.decode('ASCII').split('\n'):
if line.startswith('Chain %s ' % name):
return True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
except ssubprocess.CalledProcessError as e:
raise Fatal('%r returned %d' % (argv, e.returncode))
def ipt(family, table, *args):
if family == socket.AF_INET6:
argv = ['ip6tables', '-t', table] + list(args)
argv = ['ip6tables', '-w', '-t', table] + list(args)
elif family == socket.AF_INET:
argv = ['iptables', '-t', table] + list(args)
argv = ['iptables', '-w', '-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)
debug1('%s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env())
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def nft(family, table, action, *args):
if family == socket.AF_INET:
argv = ['nft', action, 'ip', table] + list(args)
elif family == socket.AF_INET6:
argv = ['nft', action, 'ip6', table] + list(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)
debug1('%s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env())
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def nft_get_handle(expression, chain):
cmd = 'nft'
argv = [cmd, 'list', expression, '-a']
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
for line in p.stdout:
if (b'jump %s' % chain.encode('utf-8')) in line:
return re.sub('.*# ', '', line.decode('utf-8'))
rv = p.wait()
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

@ -1,26 +1,35 @@
import os
import importlib
import socket
import struct
import errno
import ipaddress
from sshuttle.helpers import Fatal, debug3
def original_dst(sock):
ip = "0.0.0.0"
port = -1
try:
family = sock.family
SO_ORIGINAL_DST = 80
if family == socket.AF_INET:
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)
port, raw_ip = struct.unpack_from('!2xH4s', sockaddr_in[:8])
ip = str(ipaddress.IPv4Address(raw_ip))
elif family == socket.AF_INET6:
sockaddr_in = sock.getsockopt(41, SO_ORIGINAL_DST, 64)
port, raw_ip = struct.unpack_from("!2xH4x16s", sockaddr_in)
ip = str(ipaddress.IPv6Address(raw_ip))
else:
raise Fatal("fw: Unknown family type.")
except socket.error as e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
return (ip, port)
class Features(object):
@ -38,19 +47,26 @@ class BaseMethod(object):
@staticmethod
def get_supported_features():
result = Features()
result.ipv4 = True
result.ipv6 = False
result.udp = False
result.dns = True
result.user = False
return result
@staticmethod
def is_supported():
"""Returns true if it appears that this method will work on this
machine."""
return False
@staticmethod
def get_tcp_dstip(sock):
return original_dst(sock)
@staticmethod
def recv_udp(udp_listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
debug3('Accept UDP using recvfrom.')
data, srcip = udp_listener.recvfrom(bufsize)
return (srcip, None, data)
@ -68,14 +84,14 @@ class BaseMethod(object):
def assert_features(self, features):
avail = self.get_supported_features()
for key in ["udp", "dns", "ipv6", "user"]:
for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
if getattr(features, key) and not getattr(avail, key):
raise Fatal(
"Feature %s not supported with method %s.\n" %
"Feature %s not supported with method %s." %
(key, self.name))
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
user, tmark):
raise NotImplementedError()
def restore_firewall(self, port, family, udp, user):
@ -86,30 +102,21 @@ class BaseMethod(object):
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")
debug3("Selecting a method automatically...")
# Try these methods, in order:
methods_to_try = ["nat", "nft", "pf", "ipfw"]
for m in methods_to_try:
method = get_method(m)
if method.is_supported():
debug3("Method '%s' was automatically selected." % m)
return method
return get_method(method_name)
raise Fatal("Unable to automatically find a supported method. Check that "
"the appropriate programs are in your PATH. We tried "
"methods: %s" % str(methods_to_try))

View File

@ -1,24 +1,9 @@
import os
import subprocess as ssubprocess
from sshuttle.methods import BaseMethod
from sshuttle.helpers import log, debug1, debug3, \
Fatal, family_to_string
from sshuttle.helpers import log, debug1, debug2, debug3, \
Fatal, family_to_string, get_env, which
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
@ -26,11 +11,11 @@ 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))
debug3('Accept UDP python using recvmsg.')
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:
@ -39,41 +24,20 @@ if recvmsg == "python":
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)
argv = ['ipfw', 'list', '%d' % n]
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_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())
if 'check-state :sshuttle' not in line:
log('non-sshuttle ipfw rule: %r' % line.strip())
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
found = True
break
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
@ -85,11 +49,7 @@ _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)
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
for line in p.stdout:
line = line.decode()
assert(line[-1] == '\n')
@ -104,8 +64,9 @@ def _fill_oldctls(prefix):
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'))
debug1('>> %s' % ' '.join(argv))
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
# No env: No output. (Or error that won't be parsed.)
_changedctls = []
@ -118,13 +79,13 @@ def sysctl_set(name, val, permanent=False):
if not _oldctls:
_fill_oldctls(PREFIX)
if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name)
debug1('>> No such sysctl: %r' % 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')
debug1('>> ...saving permanently in /etc/sysctl.conf')
f = open('/etc/sysctl.conf', 'a')
f.write('\n'
'# Added by sshuttle\n'
@ -134,18 +95,22 @@ def sysctl_set(name, val, permanent=False):
_changedctls.append(name)
return True
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
debug1('>> %s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env())
# 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)
debug1('>> %s' % ' '.join(argv))
ssubprocess.call(argv, env=get_env())
# No env: No output. (Or error that won't be parsed.)
class Method(BaseMethod):
@ -164,7 +129,7 @@ class Method(BaseMethod):
if not dstip:
debug1(
"-- ignored UDP from %r: "
"couldn't determine destination IP address\n" % (srcip,))
"couldn't determine destination IP address" % (srcip,))
return None
return srcip, dstip, data
@ -172,15 +137,14 @@ class Method(BaseMethod):
if not srcip:
debug1(
"-- ignored UDP to %r: "
"couldn't determine source IP address\n" % (dstip,))
"couldn't determine source IP address" % (dstip,))
return
#debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
# debug3('Sending SRC: %r DST: %r' % (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()
@ -192,7 +156,7 @@ class Method(BaseMethod):
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
user, tmark):
# IPv6 not supported
if family not in [socket.AF_INET]:
raise Exception(
@ -210,8 +174,7 @@ class Method(BaseMethod):
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', 'check-state', ':sshuttle')
ipfw('add', '1', 'skipto', '2',
'tcp',
@ -219,7 +182,7 @@ class Method(BaseMethod):
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
'tcp',
'from', 'any', 'to', 'table(126)',
'not', 'ipttl', '42', 'keep-state', 'setup')
'setup', 'keep-state', ':sshuttle')
ipfw_noexit('table', '124', 'flush')
dnscount = 0
@ -230,15 +193,14 @@ class Method(BaseMethod):
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
'udp',
'from', 'any', 'to', 'table(124)',
'not', 'ipttl', '42')
'keep-state', ':sshuttle')
ipfw('add', '1', 'allow',
'udp',
'from', 'any', 'to', 'any',
'ipttl', '42')
'from', 'any', 'to', 'any')
if subnets:
# create new subnet entries
for _, swidth, sexclude, snet \
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
@ -248,10 +210,17 @@ class Method(BaseMethod):
def restore_firewall(self, port, family, udp, user):
if family not in [socket.AF_INET]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
'Address family "%s" unsupported by ipfw method'
% family_to_string(family))
ipfw_noexit('delete', '1')
ipfw_noexit('table', '124', 'flush')
ipfw_noexit('table', '125', 'flush')
ipfw_noexit('table', '126', 'flush')
def is_supported(self):
if which("ipfw"):
return True
debug2("ipfw method not supported because 'ipfw' command is "
"missing.")
return False

View File

@ -1,7 +1,7 @@
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.helpers import family_to_string, which, debug2
from sshuttle.linux import ipt, ipt_chain_exists, nonfatal
from sshuttle.methods import BaseMethod
@ -13,23 +13,18 @@ class Method(BaseMethod):
# 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:
user, tmark):
if family != socket.AF_INET and family != socket.AF_INET6:
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)
@ -50,6 +45,20 @@ class Method(BaseMethod):
_ipt('-I', 'OUTPUT', '1', *args)
_ipt('-I', 'PREROUTING', '1', *args)
# Redirect DNS traffic as requested. This includes routing traffic
# to localhost DNS servers through sshuttle.
for _, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', chain, '-j', 'REDIRECT',
'--dest', '%s' % ip,
'-p', 'udp',
'--dport', '53',
'--to-ports', str(dnsport))
# Don't route any remaining local traffic through sshuttle.
_ipt('-A', chain, '-j', 'RETURN',
'-m', 'addrtype',
'--dst-type', 'LOCAL')
# create new subnet entries.
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True):
@ -62,20 +71,13 @@ class Method(BaseMethod):
'--dest', '%s/%s' % (snet, swidth),
*tcp_ports)
else:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
_ipt('-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:
if family != socket.AF_INET and family != socket.AF_INET6:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
@ -87,9 +89,6 @@ class Method(BaseMethod):
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)
@ -111,4 +110,12 @@ class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.user = True
result.ipv6 = True
return result
def is_supported(self):
if which("iptables"):
return True
debug2("nat method not supported because 'iptables' command "
"is missing.")
return False

View File

@ -1,7 +1,8 @@
import socket
from sshuttle.firewall import subnet_weight
from sshuttle.linux import nft, nft_get_handle, nonfatal
from sshuttle.linux import nft, nonfatal
from sshuttle.methods import BaseMethod
from sshuttle.helpers import debug2, which
class Method(BaseMethod):
@ -12,23 +13,24 @@ class Method(BaseMethod):
# 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):
user, tmark):
if udp:
raise Exception("UDP not supported by nft")
table = "nat"
if family == socket.AF_INET:
table = 'sshuttle-ipv4-%s' % port
if family == socket.AF_INET6:
table = 'sshuttle-ipv6-%s' % port
def _nft(action, *args):
return nft(family, table, action, *args)
chain = 'sshuttle-%s' % port
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', 'postrouting',
'{ type nat hook postrouting priority 100; policy accept; }')
_nft('add chain', 'output',
'{ type nat hook output priority -100; policy accept; }')
_nft('add chain', chain)
@ -36,45 +38,77 @@ class Method(BaseMethod):
_nft('add rule', 'output jump %s' % chain)
_nft('add rule', 'prerouting jump %s' % chain)
# setup_firewall() gets called separately for ipv4 and ipv6. Make sure
# we only handle the version that we expect to.
if family == socket.AF_INET:
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv4', 'return')
else:
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return')
# Strings to use below to simplify our code
if family == socket.AF_INET:
ip_version_l = 'ipv4'
ip_version = 'ip'
elif family == socket.AF_INET6:
ip_version_l = 'ipv6'
ip_version = 'ip6'
# Redirect DNS traffic as requested. This includes routing traffic
# to localhost DNS servers through sshuttle.
for _, ip in [i for i in nslist if i[0] == family]:
_nft('add rule', chain, ip_version,
'daddr %s' % ip, 'udp dport 53',
('redirect to :' + str(dnsport)))
# Don't route any remaining local traffic through sshuttle
_nft('add rule', chain, 'fib daddr type local return')
# 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:
tcp_ports = tcp_ports + ('dport { %d-%d }' % (fport, lport))
# match using nfproto as described at
# https://superuser.com/questions/1560376/match-ipv6-protocol-using-nftables
if fport and fport != lport:
tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp',
'dport', '{ %d-%d }' % (fport, lport))
elif fport and fport == lport:
tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp',
'dport', '%d' % (fport))
else:
tcp_ports = ('meta', 'nfproto', ip_version_l,
'meta', 'l4proto', 'tcp')
if sexclude:
_nft('add rule', chain, *(tcp_ports + (
'ip daddr %s/%s' % (snet, swidth), 'return')))
ip_version, 'daddr %s/%s' % (snet, swidth), 'return')))
else:
_nft('add rule', chain, *(tcp_ports + (
'ip daddr %s/%s' % (snet, swidth), 'ip ttl != 42',
ip_version, 'daddr %s/%s' % (snet, swidth),
('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 = "nat"
if family == socket.AF_INET:
table = 'sshuttle-ipv4-%s' % port
if family == socket.AF_INET6:
table = 'sshuttle-ipv6-%s' % port
def _nft(action, *args):
return nft(family, table, action, *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
handle = nft_get_handle('chain ip nat output', chain)
nonfatal(_nft, 'delete rule', 'output', handle)
handle = nft_get_handle('chain ip nat prerouting', chain)
nonfatal(_nft, 'delete rule', 'prerouting', handle)
nonfatal(_nft, 'delete chain', chain)
nonfatal(_nft, 'delete table', '')
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = True
return result
def is_supported(self):
if which("nft"):
return True
debug2("nft method not supported because 'nft' command is missing.")
return False

View File

@ -3,6 +3,7 @@ import sys
import platform
import re
import socket
import errno
import struct
import subprocess as ssubprocess
import shlex
@ -10,7 +11,8 @@ 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.helpers import log, debug1, debug2, debug3, Fatal, \
family_to_string, get_env, which
from sshuttle.methods import BaseMethod
@ -124,12 +126,14 @@ class Generic(object):
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
memmove(addressof(pr) + self.ACTION_OFFSET,
struct.pack('I', self.PF_CHANGE_GET_TICKET),
4) # action = PF_CHANGE_GET_TICKET
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
memmove(addressof(pr) + self.ACTION_OFFSET,
struct.pack('I', self.PF_CHANGE_ADD_TAIL),
4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
@staticmethod
@ -151,7 +155,6 @@ class Generic(object):
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
class FreeBsd(Generic):
RULE_ACTION_OFFSET = 2968
@ -177,7 +180,8 @@ class FreeBsd(Generic):
return freebsd
def enable(self):
returncode = ssubprocess.call(['kldload', 'pf'])
returncode = ssubprocess.call(['kldload', 'pf'], env=get_env())
# No env: No output.
super(FreeBsd, self).enable()
if returncode == 0:
_pf_context['loaded_by_sshuttle'] = True
@ -186,7 +190,8 @@ class FreeBsd(Generic):
super(FreeBsd, self).disable(anchor)
if _pf_context['loaded_by_sshuttle'] and \
_pf_context['started_by_sshuttle'] == 0:
ssubprocess.call(['kldunload', 'pf'])
ssubprocess.call(['kldunload', 'pf'], env=get_env())
# No env: No output.
def add_anchors(self, anchor):
status = pfctl('-s all')[0]
@ -217,7 +222,7 @@ class FreeBsd(Generic):
b'pass out route-to lo0 %s proto tcp '
b'to %s keep state' % (inet_version, subnet)
if not exclude else
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
b'pass out %s proto tcp to %s' % (inet_version, subnet)
for exclude, subnet in includes
]
@ -261,7 +266,7 @@ class OpenBsd(Generic):
("proto_variant", c_uint8),
("direction", c_uint8)]
self.pfioc_rule = c_char * 3400
self.pfioc_rule = c_char * 3424
self.pfioc_natlook = pfioc_natlook
super(OpenBsd, self).__init__()
@ -287,7 +292,7 @@ class OpenBsd(Generic):
b'pass out %s proto tcp to %s '
b'route-to lo0 keep state' % (inet_version, subnet)
if not exclude else
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
b'pass out %s proto tcp to %s' % (inet_version, subnet)
for exclude, subnet in includes
]
@ -381,18 +386,17 @@ else:
def pfctl(args, stdin=None):
argv = ['pfctl'] + shlex.split(args)
debug1('>> %s\n' % ' '.join(argv))
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
debug1('>> %s' % ' '.join(argv))
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
stdout=ssubprocess.PIPE,
stderr=ssubprocess.PIPE,
env=env)
env=get_env())
o = p.communicate(stdin)
if p.returncode:
log('%r returned %d, stdout and stderr follows: ' %
(argv, p.returncode))
log("stdout:\n%s" % o[0].decode("ascii"))
log("stderr:\n%s" % o[1].decode("ascii"))
raise Fatal('%r returned %d' % (argv, p.returncode))
return o
@ -420,7 +424,13 @@ class Method(BaseMethod):
def get_tcp_dstip(self, sock):
pfile = self.firewall.pfile
try:
peer = sock.getpeername()
except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] == errno.EINVAL:
return sock.getsockname()
proxy = sock.getsockname()
argv = (sock.family, socket.IPPROTO_TCP,
@ -438,7 +448,7 @@ class Method(BaseMethod):
return sock.getsockname()
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
user, tmark):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by pf method_name'
@ -452,7 +462,7 @@ class Method(BaseMethod):
# exclusion first; the table will ignore the second, opposite
# definition
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True):
in sorted(subnets, key=subnet_weight):
includes.append((sexclude, b"%s/%d%s" % (
snet.encode("ASCII"),
swidth,
@ -485,3 +495,9 @@ class Method(BaseMethod):
return True
else:
return False
def is_supported(self):
if which("pfctl"):
return True
debug2("pf method not supported because 'pfctl' command is missing.")
return False

View File

@ -1,26 +1,12 @@
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.linux import ipt, ipt_chain_exists
from sshuttle.methods import BaseMethod
from sshuttle.helpers import debug1, debug3, Fatal
from sshuttle.helpers import debug1, debug2, debug3, Fatal, which
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
import os
IP_TRANSPARENT = 19
@ -30,9 +16,9 @@ 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')
debug3('Accept UDP python using recvmsg.')
data, ancdata, _, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24))
dstip = None
@ -61,44 +47,6 @@ if recvmsg == "python":
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):
@ -106,10 +54,6 @@ 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
@ -126,6 +70,15 @@ class Method(BaseMethod):
return None
return srcip, dstip, data
def setsockopt_error(self, e):
"""The tproxy method needs root permissions to successfully
set the IP_TRANSPARENT option on sockets. This method is
called when we receive a PermissionError when trying to do
so."""
raise Fatal("Insufficient permissions for tproxy method.\n"
"Your effective UID is %d, not 0. Try rerunning as root.\n"
% os.geteuid())
def send_udp(self, sock, srcip, dstip, data):
if not srcip:
debug1(
@ -134,16 +87,26 @@ class Method(BaseMethod):
return
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
except PermissionError as e:
self.setsockopt_error(e)
sender.bind(srcip)
sender.sendto(data, dstip)
sender.close()
def setup_tcp_listener(self, tcp_listener):
try:
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
except PermissionError as e:
self.setsockopt_error(e)
def setup_udp_listener(self, udp_listener):
try:
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
except PermissionError as e:
self.setsockopt_error(e)
if udp_listener.v4 is not None:
udp_listener.v4.setsockopt(
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
@ -151,7 +114,7 @@ class Method(BaseMethod):
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
user, tmark):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
@ -162,14 +125,10 @@ class Method(BaseMethod):
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
@ -185,7 +144,23 @@ class Method(BaseMethod):
_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')
# Don't have packets sent to any of our local IP addresses go
# through the tproxy or mark chains.
#
# Without this fix, if a large subnet is redirected through
# sshuttle (i.e., 0/0), then the user may be unable to receive
# UDP responses or connect to their own machine using an IP
# besides (127.0.0.1). Prior to including these lines, the
# documentation reminded the user to use -x to exclude their
# own IP addresses to receive UDP responses if they are
# redirecting a large subnet through sshuttle (i.e., 0/0).
_ipt('-A', tproxy_chain, '-j', 'RETURN', '-m', 'addrtype',
'--dst-type', 'LOCAL')
_ipt('-A', mark_chain, '-j', 'RETURN', '-m', 'addrtype',
'--dst-type', 'LOCAL')
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', tmark)
_ipt('-A', divert_chain, '-j', 'ACCEPT')
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'tcp', '-p', 'tcp')
@ -195,11 +170,11 @@ class Method(BaseMethod):
'-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',
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--tproxy-mark', tmark,
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
@ -219,12 +194,12 @@ class Method(BaseMethod):
'-m', 'tcp',
*tcp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--tproxy-mark', tmark,
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp',
*(tcp_ports + ('--on-port', str(port))))
@ -243,11 +218,12 @@ class Method(BaseMethod):
'-m', 'udp',
*udp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
'-m', 'udp',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--tproxy-mark', tmark,
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp',
*(udp_ports + ('--on-port', str(port))))
@ -263,9 +239,6 @@ class Method(BaseMethod):
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
@ -284,3 +257,10 @@ class Method(BaseMethod):
if ipt_chain_exists(family, table, divert_chain):
_ipt('-F', divert_chain)
_ipt('-X', divert_chain)
def is_supported(self):
if which("iptables") and which("ip6tables"):
return True
debug2("tproxy method not supported because 'iptables' "
"or 'ip6tables' commands are missing.\n")
return False

View File

@ -1,6 +1,7 @@
import re
import socket
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__
@ -27,29 +28,73 @@ def parse_subnetport_file(s):
# 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
#
# In addition, the port number can be specified as a range:
# 1.2.3.4:8000-8080.
#
# Can return multiple matches if the domain name used in the request
# has multiple IP addresses.
def parse_subnetport(s):
if s.count(':') > 1:
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
else:
rx = r'([\w\.]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
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()
# Ports range from fport to lport. If only one port is specified,
# fport is defined and lport is None.
#
# cidr is the mask defined with the slash notation
host, cidr, fport, lport = m.groups()
try:
addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM)
except socket.gaierror:
raise Fatal('Unable to resolve address: %s' % addr)
raise Fatal('Unable to resolve address: %s' % host)
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))
# If the address is a domain with multiple IPs and a mask is also
# provided, proceed cautiously:
if cidr is not None:
addr_v6 = [a for a in addrinfo if a[0] == socket.AF_INET6]
addr_v4 = [a for a in addrinfo if a[0] == socket.AF_INET]
return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
# Refuse to proceed if IPv4 and IPv6 addresses are present:
if len(addr_v6) > 0 and len(addr_v4) > 0:
raise Fatal("%s has IPv4 and IPv6 addresses, so the mask "
"of /%s is not supported. Specify the IP "
"addresses directly if you wish to specify "
"a mask." % (host, cidr))
# Warn if a domain has multiple IPs of the same type (IPv4 vs
# IPv6) and the mask is applied to all of the IPs.
if len(addr_v4) > 1 or len(addr_v6) > 1:
print("WARNING: %s has multiple IP addresses. The "
"mask of /%s is applied to all of the addresses."
% (host, cidr))
rv = []
for a in addrinfo:
family, _, _, _, addr = a
# Largest possible slash value we can use with this IP:
max_cidr = 32 if family == socket.AF_INET else 128
if cidr is None: # if no mask, use largest mask
cidr_to_use = max_cidr
else: # verify user-provided mask is appropriate
cidr_to_use = int(cidr)
if not 0 <= cidr_to_use <= max_cidr:
raise Fatal('Slash in CIDR notation (/%d) is '
'not between 0 and %d'
% (cidr_to_use, max_cidr))
rv.append((family, addr[0], cidr_to_use,
int(fport or 0), int(lport or fport or 0)))
return rv
# 1.2.3.4:567 or just 1.2.3.4 or just 567
@ -62,26 +107,32 @@ def parse_ipport(s):
elif ']' in s:
rx = r'(?:\[([^]]+)])(?::(\d+))?$'
else:
rx = r'([\w\.]+)(?::(\d+))?$'
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'
host, port = m.groups()
host = host or '0.0.0.0'
port = int(port or 0)
try:
addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
except socket.gaierror:
raise Fatal('%r is not a valid IP:port format' % s)
raise Fatal('Unable to resolve address: %s' % host)
if len(addrinfo) > 1:
print("WARNING: Host %s has more than one IP, only using one of them."
% host)
family, _, _, _, addr = min(addrinfo)
# Note: addr contains (ip, port)
return (family,) + addr[:2]
def parse_list(lst):
"""Parse a comma separated string into a list."""
return re.split(r'[\s,]+', lst.strip()) if lst else []
@ -96,9 +147,33 @@ class Concat(Action):
setattr(namespace, self.dest, curr_value + values)
parser = ArgumentParser(
# Override one function in the ArgumentParser so that we can have
# better control for how we parse files containing arguments. We
# expect one argument per line, but strip whitespace/quotes from the
# beginning/end of the lines.
class MyArgumentParser(ArgumentParser):
def convert_arg_line_to_args(self, arg_line):
# Ignore comments
if arg_line.startswith("#"):
return []
# strip whitespace at beginning and end of line
arg_line = arg_line.strip()
# When copying parameters from the command line to a file,
# some users might copy the quotes they used on the command
# line into the config file. We ignore these if the line
# starts and ends with the same quote.
if arg_line.startswith("'") and arg_line.endswith("'") or \
arg_line.startswith('"') and arg_line.endswith('"'):
arg_line = arg_line[1:-1]
return [arg_line]
parser = MyArgumentParser(
prog="sshuttle",
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
usage="%(prog)s [-l [ip:]port] -r [user@]sshserver[:port] <subnets...>",
fromfile_prefix_chars="@"
)
parser.add_argument(
@ -146,6 +221,7 @@ parser.add_argument(
type=parse_list,
help="""
capture and forward DNS requests made to the following servers
(comma separated)
"""
)
parser.add_argument(
@ -176,9 +252,9 @@ parser.add_argument(
)
parser.add_argument(
"-r", "--remote",
metavar="[USERNAME@]ADDR[:PORT]",
metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
help="""
ssh hostname (and optional username) of remote %(prog)s server
ssh hostname (and optional username and password) of remote %(prog)s server
"""
)
parser.add_argument(
@ -206,7 +282,7 @@ parser.add_argument(
action="count",
default=0,
help="""
increase debug message verbosity
increase debug message verbosity (can be used more than once)
"""
)
parser.add_argument(
@ -242,6 +318,16 @@ parser.add_argument(
sacrifice latency to improve bandwidth benchmarks
"""
)
parser.add_argument(
"--latency-buffer-size",
metavar="SIZE",
type=int,
default=32768,
dest="latency_buffer_size",
help="""
size of latency control buffer
"""
)
parser.add_argument(
"--wrap",
metavar="NUM",
@ -310,3 +396,51 @@ parser.add_argument(
(internal use only)
"""
)
parser.add_argument(
"--sudoers",
action="store_true",
help="""
Add sshuttle to the sudoers for this user
"""
)
parser.add_argument(
"--sudoers-no-modify",
action="store_true",
help="""
Prints the sudoers config to STDOUT and DOES NOT modify anything.
"""
)
parser.add_argument(
"--sudoers-user",
default="",
help="""
Set the user name or group with %%group_name for passwordless operation.
Default is the current user.set ALL for all users. Only works with
--sudoers or --sudoers-no-modify option.
"""
)
parser.add_argument(
"--sudoers-filename",
default="sshuttle_auto",
help="""
Set the file name for the sudoers.d file to be added. Default is
"sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option.
"""
)
parser.add_argument(
"--no-sudo-pythonpath",
action="store_false",
dest="sudo_pythonpath",
help="""
do not set PYTHONPATH when invoking sudo
"""
)
parser.add_argument(
"-t", "--tmark",
metavar="[MARK]",
default="0x01",
help="""
tproxy optional traffic mark with provided MARK value in
hexadecimal (default '0x01')
"""
)

View File

@ -1,8 +1,21 @@
"""When sshuttle is run via a systemd service file, we can communicate
to systemd about the status of the sshuttle process. In particular, we
can send READY status to tell systemd that sshuttle has completed
startup and send STOPPING to indicate that sshuttle is beginning
shutdown.
For details, see:
https://www.freedesktop.org/software/systemd/man/sd_notify.html
"""
import socket
import os
from sshuttle.helpers import debug1
def _notify(message):
"""Send a notification message to systemd."""
addr = os.environ.get("NOTIFY_SOCKET", None)
if not addr or len(addr) == 1 or addr[0] not in ('/', '@'):
@ -13,7 +26,7 @@ def _notify(message):
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)
debug1("Error creating socket to notify systemd: %s" % e)
return False
if not message:
@ -24,17 +37,27 @@ def _notify(message):
try:
return (sock.sendto(message, addr) > 0)
except (OSError, IOError) as e:
debug1("Error notifying systemd: %s\n" % e)
debug1("Error notifying systemd: %s" % e)
return False
def send(*messages):
"""Send multiple messages to systemd."""
return _notify(b'\n'.join(messages))
def ready():
"""Constructs a message that is appropriate to send upon completion of
sshuttle startup."""
return b"READY=1"
def stop():
"""Constructs a message that is appropriate to send when sshuttle is
beginning to shutdown."""
return b"STOPPING=1"
def status(message):
"""Constructs a status message to be sent to systemd."""
return b"STATUS=%s" % message.encode('utf8')

View File

@ -5,7 +5,7 @@ import traceback
import time
import sys
import os
import platform
import sshuttle.ssnet as ssnet
import sshuttle.helpers as helpers
@ -13,19 +13,14 @@ 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
try:
from shutil import which
except ImportError:
from distutils.spawn import find_executable as which
resolvconf_random_nameserver, which, get_env
def _ipmatch(ipstr):
# FIXME: IPv4 only
if ipstr == 'default':
ipstr = '0.0.0.0/0'
m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m:
g = m.groups()
ips = g[0]
@ -86,11 +81,7 @@ def _route_iproute(line):
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)
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
routes = []
for line in p.stdout:
if not line.strip():
@ -104,8 +95,7 @@ def _list_routes(argv, extract_route):
(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')
log('WARNING: %r returned %d' % (argv, rv))
return routes
@ -116,7 +106,8 @@ def list_routes():
elif which('netstat'):
routes = _list_routes(['netstat', '-rn'], _route_netstat)
else:
log('WARNING: Neither ip nor netstat were found on the server.\n')
log('WARNING: Neither "ip" nor "netstat" were found on the server. '
'--auto-nets feature will not work.')
routes = []
for (family, ip, width) in routes:
@ -143,7 +134,7 @@ def start_hostwatch(seed_hosts, auto_hosts):
s1.close()
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
except Exception:
log('%s\n' % _exc_dump())
log('%s' % _exc_dump())
rv = 98
finally:
os._exit(rv)
@ -191,7 +182,7 @@ class DnsProxy(Handler):
self.tries += 1
if self.to_nameserver is None:
_, peer = resolvconf_random_nameserver()
_, peer = resolvconf_random_nameserver(False)
port = 53
else:
peer = self.to_ns_peer
@ -199,12 +190,11 @@ class DnsProxy(Handler):
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))
debug2('DNS: sending to %r:%d (try %d)' % (peer, port, self.tries))
try:
sock.send(self.request)
self.socks.append(sock)
@ -214,11 +204,11 @@ class DnsProxy(Handler):
# 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))
debug2('DNS send to %r: %s' % (peer, e))
self.try_send()
return
else:
log('DNS send to %r: %s\n' % (peer, e))
log('DNS send to %r: %s' % (peer, e))
return
def callback(self, sock):
@ -235,13 +225,13 @@ class DnsProxy(Handler):
# 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))
debug2('DNS recv from %r: %s' % (peer, e))
self.try_send()
return
else:
log('DNS recv from %r: %s\n' % (peer, e))
log('DNS recv from %r: %s' % (peer, e))
return
debug2('DNS response: %d bytes\n' % len(data))
debug2('DNS response: %d bytes' % len(data))
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
self.ok = False
@ -255,16 +245,14 @@ class UdpProxy(Handler):
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)
debug2('UDP: sending to %r port %d' % 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))
log('UDP send to %r port %d: %s' % (dstip[0], dstip[1], e))
return
def callback(self, sock):
@ -272,38 +260,40 @@ class UdpProxy(Handler):
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))
log('UDP recv from %r port %d: %s' % (peer[0], peer[1], e))
return
debug2('UDP response: %d bytes\n' % len(data))
debug2('UDP response: %d bytes' % 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):
debug1('Starting server with Python version %s\n'
% platform.python_version())
if helpers.verbose >= 1:
def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
auto_nets):
try:
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(' %d/%s/%d\n' % r)
debug1('latency control setting = %r' % latency_control)
if latency_buffer_size:
import sshuttle.ssnet as ssnet
ssnet.LATENCY_BUFFER_SIZE = latency_buffer_size
# synchronization header
sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush()
handlers = []
mux = Mux(socket.fromfd(sys.stdin.fileno(),
socket.AF_INET, socket.SOCK_STREAM),
socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM))
mux = Mux(sys.stdin, sys.stdout)
handlers.append(mux)
debug1('auto-nets:' + str(auto_nets))
if auto_nets:
routes = list(list_routes())
debug1('available routes:')
for r in routes:
debug1(' %d/%s/%d' % r)
else:
routes = []
routepkt = ''
for r in routes:
routepkt += '%d,%s,%d\n' % r
@ -352,7 +342,7 @@ def main(latency_control, auto_hosts, to_nameserver):
dnshandlers = {}
def dns_req(channel, data):
debug2('Incoming DNS request channel=%d.\n' % channel)
debug2('Incoming DNS request channel=%d.' % channel)
h = DnsProxy(mux, channel, data, to_nameserver)
handlers.append(h)
dnshandlers[channel] = h
@ -361,25 +351,28 @@ def main(latency_control, auto_hosts, to_nameserver):
udphandlers = {}
def udp_req(channel, cmd, data):
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
debug2('Incoming UDP request channel=%d, cmd=%d' %
(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))
debug2('is incoming UDP data. %r %d.' % (dstip, dstport))
h = udphandlers[channel]
h.send((dstip, dstport), data)
elif cmd == ssnet.CMD_UDP_CLOSE:
debug2('is incoming UDP close\n')
debug2('is incoming UDP close')
h = udphandlers[channel]
h.ok = False
del mux.channels[channel]
def udp_open(channel, data):
debug2('Incoming UDP open.\n')
debug2('Incoming UDP open.')
family = int(data)
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, 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)
raise Fatal('UDP connection channel %d already open' %
channel)
else:
h = UdpProxy(mux, channel, family)
handlers.append(h)
@ -392,7 +385,7 @@ def main(latency_control, auto_hosts, to_nameserver):
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
'hostwatch exited unexpectedly: code 0x%04x' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
@ -403,7 +396,7 @@ def main(latency_control, auto_hosts, to_nameserver):
remove = []
for channel, h in dnshandlers.items():
if h.timeout < now or not h.ok:
debug3('expiring dnsreqs channel=%d\n' % channel)
debug3('expiring dnsreqs channel=%d' % channel)
remove.append(channel)
h.ok = False
for channel in remove:
@ -412,8 +405,12 @@ def main(latency_control, auto_hosts, to_nameserver):
remove = []
for channel, h in udphandlers.items():
if not h.ok:
debug3('expiring UDP channel=%d\n' % channel)
debug3('expiring UDP channel=%d' % channel)
remove.append(channel)
h.ok = False
for channel in remove:
del udphandlers[channel]
except Fatal as e:
log('fatal: %s' % e)
sys.exit(99)

View File

@ -3,89 +3,96 @@ import os
import re
import socket
import zlib
import imp
import importlib
import importlib.util
import subprocess as ssubprocess
import shlex
import sshuttle.helpers as helpers
from sshuttle.helpers import debug2
try:
# Python >= 3.5
from shlex import quote
except ImportError:
# Python 2.x
from pipes import quote
import ipaddress
from urllib.parse import urlparse
import sshuttle.helpers as helpers
from sshuttle.helpers import debug2, which, get_path, Fatal
def readfile(name):
tokens = name.split(".")
f = None
token = tokens[0]
token_name = [token]
token_str = ".".join(token_name)
try:
f, pathname, description = imp.find_module(token_str)
for token in tokens[1:]:
module = imp.load_module(token_str, f, pathname, description)
if f is not None:
f.close()
token_name.append(token)
token_str = ".".join(token_name)
f, pathname, description = imp.find_module(
token, module.__path__)
if f is not None:
contents = f.read()
else:
contents = ""
finally:
if f is not None:
f.close()
return contents.encode("UTF8")
def get_module_source(name):
spec = importlib.util.find_spec(name)
with open(spec.origin, "rt") as f:
return f.read().encode("utf-8")
def empackage(z, name, data=None):
if not data:
data = readfile(name)
data = get_module_source(name)
content = z.compress(data)
content += z.flush(zlib.Z_SYNC_FLUSH)
return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content)
def parse_hostport(rhostport):
"""
parses the given rhostport variable, looking like this:
[username[:password]@]host[:port]
if only host is given, can be a hostname, IPv4/v6 address or a ssh alias
from ~/.ssh/config
and returns a tuple (username, password, port, host)
"""
# leave use of default port to ssh command to prevent overwriting
# ports configured in ~/.ssh/config when no port is given
if rhostport is None or len(rhostport) == 0:
return None, None, None, None
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 address with port specification,
# then it will look like: [::1]:22
try:
# try to parse host as an IP address,
# if that works it is an IPv6 address
host = str(ipaddress.ip_address(host))
except ValueError:
# if that fails parse as URL to get the port
parsed = urlparse('//{}'.format(host))
try:
host = str(ipaddress.ip_address(parsed.hostname))
except ValueError:
# else if both fails, we have a hostname with port
host = parsed.hostname
port = parsed.port
if password is None or len(password) == 0:
password = None
return username, password, port, host
def connect(ssh_cmd, rhostport, python, stderr, options):
portl = []
if re.sub(r'.*@', '', rhostport or '').count(':') > 1:
if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']')
rhost = result[0].strip('[')
if len(result) > 1:
result[1] = result[1].strip(':')
if result[1] != '':
portl = ['-p', str(int(result[1]))]
# can't disambiguate IPv6 colons and a port number. pass the hostname
# through.
username, password, port, host = parse_hostport(rhostport)
if username:
rhost = "{}@{}".format(username, host)
else:
rhost = rhostport
else: # IPv4
l = (rhostport or '').rsplit(':', 1)
rhost = l[0]
if len(l) > 1:
portl = ['-p', str(int(l[1]))]
if rhost == '-':
rhost = None
rhost = host
z = zlib.compressobj(1)
content = readfile('sshuttle.assembler')
content = get_module_source('sshuttle.assembler')
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
optdata = optdata.encode("UTF8")
content2 = (empackage(z, 'sshuttle') +
@ -96,11 +103,21 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
empackage(z, 'sshuttle.server') +
b"\n")
# If the exec() program calls sys.exit(), it should exit python
# and the sys.exit(98) call won't be reached (so we try to only
# exit that way in the server). However, if the code that we
# exec() simply returns from main, then we will return from
# exec(). If the server's python process dies, it should stop
# executing and also won't reach sys.exit(98).
#
# So, we shouldn't reach sys.exit(98) and we certainly shouldn't
# reach it immediately after trying to start the server.
pyscript = r"""
import sys, os;
verbosity=%d;
sys.stdin = os.fdopen(0, "rb");
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"));
sys.exit(98);
""" % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@ -113,15 +130,77 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
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>/dev/null || P=python; "
"exec \"$P\" -c %s") % quote(pyscript)
pycmd = ("exec /bin/sh -c %s" % quote(pycmd))
# By default, we run the following code in a shell.
# However, with restricted shells and other unusual
# situations, there can be trouble. See the RESTRICTED
# SHELL section in "man bash" for more information. The
# code makes many assumptions:
#
# (1) That /bin/sh exists and that we can call it.
# Restricted shells often do *not* allow you to run
# programs specified with an absolute path like /bin/sh.
# Either way, if there is trouble with this, it should
# return error code 127.
#
# (2) python3 or python exists in the PATH and is
# executable. If they aren't, then exec won't work (see (4)
# below).
#
# (3) In /bin/sh, that we can redirect stderr in order to
# hide the version that "python3 -V" might print (some
# restricted shells don't allow redirection, see
# RESTRICTED SHELL section in 'man bash'). However, if we
# are in a restricted shell, we'd likely have trouble with
# assumption (1) above.
#
# (4) The 'exec' command should work except if we failed
# to exec python because it doesn't exist or isn't
# executable OR if exec isn't allowed (some restricted
# shells don't allow exec). If the exec succeeded, it will
# not return and not get to the "exit 97" command. If exec
# does return, we exit with code 97.
#
# Specifying the exact python program to run with --python
# avoids many of the issues above. However, if
# you have a restricted shell on remote, you may only be
# able to run python if it is in your PATH (and you can't
# run programs specified with an absolute path). In that
# case, sshuttle might not work at all since it is not
# possible to run python on the remote machine---even if
# it is present.
pycmd = ("P=python3; $P -V 2>%s || P=python; "
"exec \"$P\" -c %s; exit 97") % \
(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])
# Our which() function searches for programs in get_path()
# directories (which include PATH). This step isn't strictly
# necessary if ssh is already in the user's PATH, but it makes the
# error message friendlier if the user incorrectly passes in a
# custom ssh command that we cannot find.
abs_path = which(argv[0])
if abs_path is None:
raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path()))
argv[0] = abs_path
(s1, s2) = socket.socketpair()
def setup():
@ -129,7 +208,8 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
s2.close()
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close()
debug2('executing: %r\n' % argv)
debug2('executing: %r' % argv)
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True, stderr=stderr)
os.close(s1a)

View File

@ -4,19 +4,19 @@ import socket
import errno
import select
import os
from sshuttle.helpers import b, binary_type, log, debug1, debug2, debug3, Fatal
import fcntl
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
MAX_CHANNEL = 65535
LATENCY_BUFFER_SIZE = 32768
# these don't exist in the socket module in python 2.3!
SHUT_RD = 0
SHUT_WR = 1
SHUT_RDWR = 2
HDR_LEN = 8
CMD_EXIT = 0x4200
CMD_PING = 0x4201
CMD_PONG = 0x4202
@ -55,17 +55,18 @@ cmd_to_name = {
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
errno.EHOSTUNREACH, errno.ENETUNREACH,
errno.EHOSTDOWN, errno.ENETDOWN,
errno.ENETUNREACH]
errno.ENETUNREACH, errno.ECONNABORTED,
errno.ECONNRESET]
def _add(l, elem):
if elem not in l:
l.append(elem)
def _add(socks, elem):
if elem not in socks:
socks.append(elem)
def _fds(l):
def _fds(socks):
out = []
for i in l:
for i in socks:
try:
out.append(i.fileno())
except AttributeError:
@ -82,7 +83,7 @@ def _nb_clean(func, *args):
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
raise
else:
debug3('%s: err was: %s\n' % (func.__name__, e))
debug3('%s: err was: %s' % (func.__name__, e))
return None
@ -93,8 +94,12 @@ def _try_peername(sock):
return '%s:%s' % (pn[0], pn[1])
except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
if e.args[0] == errno.EINVAL:
pass
elif e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
raise
except AttributeError:
pass
return 'unknown'
@ -106,7 +111,7 @@ class SockWrapper:
def __init__(self, rsock, wsock, connect_to=None, peername=None):
global _swcount
_swcount += 1
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
debug3('creating new SockWrapper (%d now exist)' % _swcount)
self.exc = None
self.rsock = rsock
self.wsock = wsock
@ -119,9 +124,9 @@ class SockWrapper:
def __del__(self):
global _swcount
_swcount -= 1
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
debug1('%r: deleting (%d remain)' % (self, _swcount))
if self.exc:
debug1('%r: error was: %s\n' % (self, self.exc))
debug1('%r: error was: %s' % (self, self.exc))
def __repr__(self):
if self.rsock == self.wsock:
@ -143,14 +148,14 @@ class SockWrapper:
if not self.connect_to:
return # already connected
self.rsock.setblocking(False)
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
debug3('%r: trying connect to %r' % (self, self.connect_to))
try:
self.rsock.connect(self.connect_to)
# connected successfully (Linux)
self.connect_to = None
except socket.error:
_, e = sys.exc_info()[:2]
debug3('%r: connect result: %s\n' % (self, e))
debug3('%r: connect result: %s' % (self, e))
if e.args[0] == errno.EINVAL:
# this is what happens when you call connect() on a socket
# that is now connected but returned EINPROGRESS last time,
@ -160,7 +165,7 @@ class SockWrapper:
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
socket.SO_ERROR)
e = socket.error(realerr, os.strerror(realerr))
debug3('%r: fixed connect result: %s\n' % (self, e))
debug3('%r: fixed connect result: %s' % (self, e))
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet
elif e.args[0] == 0:
@ -186,13 +191,13 @@ class SockWrapper:
def noread(self):
if not self.shut_read:
debug2('%r: done reading\n' % self)
debug2('%r: done reading' % self)
self.shut_read = True
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
def nowrite(self):
if not self.shut_write:
debug2('%r: done writing\n' % self)
debug2('%r: done writing' % self)
self.shut_write = True
try:
self.wsock.shutdown(SHUT_WR)
@ -213,7 +218,7 @@ class SockWrapper:
except OSError:
_, e = sys.exc_info()[:2]
if e.errno == errno.EPIPE:
debug1('%r: uwrite: got EPIPE\n' % self)
debug1('%r: uwrite: got EPIPE' % self)
self.nowrite()
return 0
else:
@ -270,12 +275,12 @@ class Handler:
_add(r, i)
def callback(self, sock):
log('--no callback defined-- %r\n' % self)
log('--no callback defined-- %r' % self)
(r, _, _) = select.select(self.socks, [], [], 0)
for s in r:
v = s.recv(4096)
if not v:
log('--closed-- %r\n' % self)
log('--closed-- %r' % self)
self.socks = []
self.ok = False
@ -332,10 +337,10 @@ class Proxy(Handler):
class Mux(Handler):
def __init__(self, rsock, wsock):
Handler.__init__(self, [rsock, wsock])
self.rsock = rsock
self.wsock = wsock
def __init__(self, rfile, wfile):
Handler.__init__(self, [rfile, wfile])
self.rfile = rfile
self.wfile = wfile
self.new_channel = self.got_dns_req = self.got_routes = None
self.got_udp_open = self.got_udp_data = self.got_udp_close = None
self.got_host_req = self.got_host_list = None
@ -364,7 +369,7 @@ class Mux(Handler):
return total
def check_fullness(self):
if self.fullness > 32768:
if self.fullness > LATENCY_BUFFER_SIZE:
if not self.too_full:
self.send(0, CMD_PING, b('rttest'))
self.too_full = True
@ -372,26 +377,26 @@ class Mux(Handler):
# 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' % (self.amount_queued(), ob))
def send(self, channel, cmd, data):
assert isinstance(data, binary_type)
assert isinstance(data, bytes)
assert len(data) <= 65535
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
+ data
self.outbuf.append(p)
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
% (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'
debug2('< channel=%d cmd=%s len=%d'
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
if cmd == CMD_PING:
self.send(0, CMD_PONG, data)
elif cmd == CMD_PONG:
debug2('received PING response\n')
debug2('received PING response')
self.too_full = False
self.fullness = 0
elif cmd == CMD_EXIT:
@ -426,29 +431,44 @@ class Mux(Handler):
else:
callback = self.channels.get(channel)
if not callback:
log('warning: closed channel %d got cmd=%s len=%d\n'
log('warning: closed channel %d got cmd=%s len=%d'
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
else:
callback(cmd, data)
def flush(self):
self.wsock.setblocking(False)
try:
os.set_blocking(self.wfile.fileno(), False)
except AttributeError:
# python < 3.5
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL)
flags |= os.O_NONBLOCK
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
if self.outbuf and self.outbuf[0]:
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0])))
if wrote:
self.outbuf[0] = self.outbuf[0][wrote:]
while self.outbuf and not self.outbuf[0]:
self.outbuf[0:1] = []
def fill(self):
self.rsock.setblocking(False)
try:
read = _nb_clean(os.read, self.rsock.fileno(), 32768)
os.set_blocking(self.rfile.fileno(), False)
except AttributeError:
# python < 3.5
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL)
flags |= os.O_NONBLOCK
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
try:
# If LATENCY_BUFFER_SIZE is inappropriately large, we will
# get a MemoryError here. Read no more than 1MiB.
read = _nb_clean(os.read, self.rfile.fileno(),
min(1048576, LATENCY_BUFFER_SIZE))
except OSError:
_, e = sys.exc_info()[:2]
raise Fatal('other end: %r' % e)
# log('<<< %r\n' % b)
# log('<<< %r' % b)
if read == b(''): # EOF
self.ok = False
if read:
@ -456,7 +476,7 @@ class Mux(Handler):
def handle(self):
self.fill()
# log('inbuf is: (%d,%d) %r\n'
# log('inbuf is: (%d,%d) %r'
# % (self.want, len(self.inbuf), self.inbuf))
while 1:
if len(self.inbuf) >= (self.want or HDR_LEN):
@ -474,27 +494,27 @@ class Mux(Handler):
break
def pre_select(self, r, w, x):
_add(r, self.rsock)
_add(r, self.rfile)
if self.outbuf:
_add(w, self.wsock)
_add(w, self.wfile)
def callback(self, sock):
(r, w, _) = select.select([self.rsock], [self.wsock], [], 0)
if self.rsock in r:
(r, w, _) = select.select([self.rfile], [self.wfile], [], 0)
if self.rfile in r:
self.handle()
if self.outbuf and self.wsock in w:
if self.outbuf and self.wfile in w:
self.flush()
class MuxWrapper(SockWrapper):
def __init__(self, mux, channel):
SockWrapper.__init__(self, mux.rsock, mux.wsock)
SockWrapper.__init__(self, mux.rfile, mux.wfile)
self.mux = mux
self.channel = channel
self.mux.channels[channel] = self.got_packet
self.socks = []
debug2('new channel: %d\n' % channel)
debug2('new channel: %d' % channel)
def __del__(self):
self.nowrite()
@ -510,7 +530,7 @@ class MuxWrapper(SockWrapper):
def setnoread(self):
if not self.shut_read:
debug2('%r: done reading\n' % self)
debug2('%r: done reading' % self)
self.shut_read = True
self.maybe_close()
@ -521,13 +541,13 @@ class MuxWrapper(SockWrapper):
def setnowrite(self):
if not self.shut_write:
debug2('%r: done writing\n' % self)
debug2('%r: done writing' % self)
self.shut_write = True
self.maybe_close()
def maybe_close(self):
if self.shut_read and self.shut_write:
debug2('%r: closing connection\n' % self)
debug2('%r: closing connection' % 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
@ -564,9 +584,9 @@ class MuxWrapper(SockWrapper):
def connect_dst(family, ip, port):
debug2('Connecting to %s:%d\n' % (ip, port))
debug2('Connecting to %s:%d' % (ip, port))
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))
@ -582,11 +602,11 @@ def runonce(handlers, mux):
for s in handlers:
s.pre_select(r, w, x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)'
% (len(handlers), _fds(r), _fds(w), _fds(x),
mux.fullness, mux.too_full))
(r, w, x) = select.select(r, w, x)
debug2(' Ready: %d r=%r w=%r x=%r\n'
debug2(' Ready: %d r=%r w=%r x=%r'
% (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r + w + x
did = {}

View File

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

71
sshuttle/sudoers.py Normal file
View File

@ -0,0 +1,71 @@
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
'''
warning_msg = "# WARNING: When you allow a user to run sshuttle as root,\n" \
"# they can then use sshuttle's --ssh-cmd option to run any\n" \
"# command as root.\n"
def build_config(user_name):
content = warning_msg
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]
sys.stdout.write(streamdata.decode("ASCII"))
returncode = process.returncode
if returncode:
log('Failed updating sudoers file.')
debug1(streamdata)
exit(returncode)
else:
log('Success, sudoers file update.')
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:
sys.stdout.write(warning_msg)
save_config(content, file_name)

View File

@ -1,7 +1,7 @@
from mock import Mock, patch, call
import io
from socket import AF_INET, AF_INET6
from unittest.mock import Mock, patch, call
import sshuttle.firewall
@ -15,7 +15,7 @@ NSLIST
{inet},1.2.3.33
{inet6},2404:6800:4004:80c::33
PORTS 1024,1025,1026,1027
GO 1 -
GO 1 - 0x01 12345
HOST 1.2.3.3,existing
""".format(inet=AF_INET, inet6=AF_INET6))
stdout = Mock()
@ -55,7 +55,7 @@ def test_rewrite_etc_hosts(tmpdir):
assert line == ""
with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
sshuttle.firewall.restore_etc_hosts(10)
sshuttle.firewall.restore_etc_hosts(hostmap, 10)
assert orig_hosts.computehash() == new_hosts.computehash()
@ -85,8 +85,9 @@ def test_subnet_weight():
(AF_INET, 0, 1, '0.0.0.0', 0, 0)
]
assert subnets_sorted == \
sorted(subnets, key=sshuttle.firewall.subnet_weight, reverse=True)
assert subnets_sorted == sorted(subnets,
key=sshuttle.firewall.subnet_weight,
reverse=True)
@patch('sshuttle.firewall.rewrite_etc_hosts')
@ -115,6 +116,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
assert mock_setup_daemon.mock_calls == [call()]
assert mock_get_method.mock_calls == [
call('not_auto'),
call().is_supported(),
call().is_supported().__bool__(),
call().setup_firewall(
1024, 1026,
[(AF_INET6, u'2404:6800:4004:80c::33')],
@ -122,7 +125,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
[(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),
None,
'0x01'),
call().setup_firewall(
1025, 1027,
[(AF_INET, u'1.2.3.33')],
@ -130,7 +134,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
True,
None),
None,
'0x01'),
call().restore_firewall(1024, AF_INET6, True, None),
call().restore_firewall(1025, AF_INET, True, None),
]

View File

@ -1,10 +1,9 @@
from mock import patch, call
import sys
import io
import socket
from socket import AF_INET, AF_INET6
import errno
from unittest.mock import patch, call
import sshuttle.helpers
@ -25,19 +24,19 @@ def test_log(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\r\n'),
call.flush(),
call.write('prefix: abc'),
call.write('prefix: abc\r\n'),
call.flush(),
call.write('prefix: message 1\n'),
call.write('prefix: message 1\r\n'),
call.flush(),
call.write('prefix: message 2\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.write('prefix: message 2\r\n'),
call.write(' line2\r\n'),
call.write(' line3\r\n'),
call.flush(),
call.write('prefix: message 3\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.write('prefix: message 3\r\n'),
call.write(' line2\r\n'),
call.write(' line3\r\n'),
call.flush(),
]
@ -52,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\r\n'),
call.flush(),
]
@ -77,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\r\n'),
call.flush(),
]
@ -102,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\r\n'),
call.flush(),
]
@ -132,7 +131,7 @@ nameserver 2404:6800:4004:80c::3
nameserver 2404:6800:4004:80c::4
""")
ns = sshuttle.helpers.resolvconf_nameservers()
ns = sshuttle.helpers.resolvconf_nameservers(False)
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'),
@ -157,7 +156,7 @@ nameserver 2404:6800:4004:80c::2
nameserver 2404:6800:4004:80c::3
nameserver 2404:6800:4004:80c::4
""")
ns = sshuttle.helpers.resolvconf_random_nameserver()
ns = sshuttle.helpers.resolvconf_random_nameserver(False)
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'),
@ -193,9 +192,5 @@ def test_family_ip_tuple():
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"
if sys.version_info < (3, 0):
expected = "1"
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1"
else:
expected = 'AddressFamily.AF_UNIX'
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected

View File

@ -1,9 +1,9 @@
import pytest
from mock import Mock, patch, call
import socket
from socket import AF_INET, AF_INET6
import struct
import pytest
from unittest.mock import Mock, patch, call
from sshuttle.helpers import Fatal
from sshuttle.methods import get_method
@ -11,19 +11,29 @@ 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 features.ipv6
assert not features.udp
assert features.dns
def test_get_tcp_dstip():
sock = Mock()
sock.family = AF_INET
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)]
sock = Mock()
sock.family = AF_INET6
sock.getsockopt.return_value = struct.pack(
'!HH4xBBBBBBBBBBBBBBBB', socket.ntohs(AF_INET6),
1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)
method = get_method('nft')
assert method.get_tcp_dstip(sock) == ('::1', 1024)
assert sock.mock_calls == [call.getsockopt(41, 80, 64)]
def test_recv_udp():
sock = Mock()
@ -75,26 +85,52 @@ def test_firewall_command():
@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):
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
mock_ipt_chain_exists.return_value = True
method = get_method('nat')
assert method.name == 'nat'
with pytest.raises(Exception) as excinfo:
assert mock_ipt_chain_exists.mock_calls == []
assert mock_ipt.mock_calls == []
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'
False,
None,
'0x01')
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET6, 'nat', 'sshuttle-1024')
]
assert mock_ipt.mock_calls == [
call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-F', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-X', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-N', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-F', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1024'),
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT',
'--dest', u'2404:6800:4004:80c::33', '-p', 'udp',
'--dport', '53', '--to-ports', '1026'),
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '-p', 'tcp',
'--dport', '80:80'),
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT',
'--dest', u'2404:6800:4004:80c::/64', '-p', 'tcp',
'--to-ports', '1024')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt.reset_mock()
assert mock_ipt_chain_exists.mock_calls == []
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == []
with pytest.raises(Exception) as excinfo:
@ -105,10 +141,10 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
True,
None)
None,
'0x01')
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(
@ -118,18 +154,11 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
False,
None)
None,
'0x01')
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'),
@ -139,24 +168,45 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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', 'REDIRECT',
'--dest', u'1.2.3.33', '-p', 'udp',
'--dport', '53', '--to-ports', '1027'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
'--to-ports', '1025')
]
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', '-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()
method.restore_firewall(1025, AF_INET6, False, None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET6, 'nat', 'sshuttle-1025')
]
assert mock_ipt.mock_calls == [
call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j',
'sshuttle-1025'),
call(AF_INET6, 'nat', '-F', 'sshuttle-1025'),
call(AF_INET6, 'nat', '-X', 'sshuttle-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt.reset_mock()

View File

@ -1,10 +1,10 @@
import pytest
from mock import Mock, patch, call, ANY
import socket
from socket import AF_INET, AF_INET6
import pytest
from unittest.mock import Mock, patch, call, ANY
from sshuttle.methods import get_method
from sshuttle.helpers import Fatal
from sshuttle.helpers import Fatal, get_env
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
@ -186,7 +186,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(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)
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -205,10 +206,10 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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 quick inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\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'),
@ -225,7 +226,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -238,7 +240,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -257,8 +260,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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 quick inet proto tcp to 1.2.3.66/32 port 80:80\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'),
@ -298,7 +301,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
[(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)
None,
'0x01')
assert mock_pfctl.mock_calls == [
call('-s all'),
@ -308,15 +312,16 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
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 quick inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\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
assert call(['kldload', 'pf'], env=get_env()) in \
mock_subprocess_call.mock_calls
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
@ -329,7 +334,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -342,7 +348,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
@ -359,8 +366,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
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 quick inet proto tcp to 1.2.3.66/32 port 80:80\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'),
@ -400,11 +407,12 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(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)
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
@ -416,10 +424,10 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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 quick inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\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'),
@ -436,7 +444,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -449,10 +458,11 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
@ -464,8 +474,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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 quick inet proto tcp to 1.2.3.66/32 port 80:80\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'),

View File

@ -1,13 +1,12 @@
import socket
from socket import AF_INET, AF_INET6
from mock import Mock, patch, call
from unittest.mock import Mock, patch, call
from sshuttle.methods import get_method
@patch("sshuttle.methods.tproxy.recvmsg")
def test_get_supported_features_recvmsg(mock_recvmsg):
def test_get_supported_features():
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
@ -15,15 +14,6 @@ def test_get_supported_features_recvmsg(mock_recvmsg):
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)
@ -92,9 +82,8 @@ def test_firewall_command():
@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):
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
mock_ipt_chain_exists.return_value = True
method = get_method('tproxy')
assert method.name == 'tproxy'
@ -108,13 +97,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
[(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)
None,
'0x01')
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'),
@ -133,18 +122,22 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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-t-1024', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
'--set-mark', '1'),
'--set-mark', '0x01'),
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',
'--set-mark', '0x01', '--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',
'--tproxy-mark', '0x01',
'--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',
@ -160,22 +153,23 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'--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',
'--set-mark', '0x01', '--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',
'--tproxy-mark', '0x01', '--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'),
'--set-mark', '0x01', '--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',
'--tproxy-mark', '0x01', '--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)
@ -184,7 +178,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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'),
@ -196,7 +189,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
# IPV4
@ -208,13 +200,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
'0x01')
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'),
@ -233,18 +225,22 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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-t-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
'-j', 'MARK', '--set-mark', '1'),
'-j', 'MARK', '--set-mark', '0x01'),
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',
'--set-mark', '0x01', '--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',
'--tproxy-mark', '0x01', '--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',
@ -259,20 +255,19 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'--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',
'--set-mark', '0x01', '--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',
'--tproxy-mark', '0x01', '--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',
'--set-mark', '0x01', '--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',
'--tproxy-mark', '0x01', '--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)
@ -281,7 +276,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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'),
@ -293,5 +287,4 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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

@ -1,8 +1,10 @@
import socket
import pytest
import sshuttle.options
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',
@ -11,7 +13,6 @@ _ip4_reprs = {
'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)
@ -25,10 +26,11 @@ _ip6_reprs = {
_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)
== [(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'
@ -39,34 +41,35 @@ def test_parse_subnetport_ip4_with_mask():
for swidth in _ip4_swidths:
assert sshuttle.options.parse_subnetport(
'/'.join((ip_repr, str(swidth)))
) == (socket.AF_INET, ip, swidth, 0, 0)
) == [(socket.AF_INET, ip, swidth, 0, 0)]
assert sshuttle.options.parse_subnetport('0/0') \
== (socket.AF_INET, '0.0.0.0', 0, 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'
assert str(excinfo.value) \
== 'Slash in CIDR notation (/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)
== [(socket.AF_INET, ip, 32, 80, 80)]
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\
== (socket.AF_INET, ip, 32, 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)
== [(socket.AF_INET, ip, 32, 80, 80)]
assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \
== (socket.AF_INET, ip, 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)
== [(socket.AF_INET6, ip, 128, 0, 0)]
def test_parse_subnetport_ip6_with_mask():
@ -74,25 +77,31 @@ def test_parse_subnetport_ip6_with_mask():
for swidth in _ip4_swidths + _ip6_swidths:
assert sshuttle.options.parse_subnetport(
'/'.join((ip_repr, str(swidth)))
) == (socket.AF_INET6, ip, swidth, 0, 0)
) == [(socket.AF_INET6, ip, swidth, 0, 0)]
assert sshuttle.options.parse_subnetport('::/0') \
== (socket.AF_INET6, '::', 0, 0, 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'
assert str(excinfo.value) \
== 'Slash in CIDR notation (/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)
== [(socket.AF_INET6, ip, 128, 80, 80)]
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \
== (socket.AF_INET6, ip, 128, 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)
== [(socket.AF_INET6, ip, 128, 80, 80)]
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
== (socket.AF_INET6, ip, 16, 80, 90)
== [(socket.AF_INET6, ip, 16, 80, 90)]
def test_convert_arg_line_to_args_skips_comments():
parser = sshuttle.options.MyArgumentParser()
assert parser.convert_arg_line_to_args("# whatever something") == []

View File

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

View File

@ -1,7 +1,9 @@
import io
import socket
from unittest.mock import patch, Mock
import sshuttle.server
from mock import patch, Mock
def test__ipmatch():

View File

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

22
tox.ini
View File

@ -1,21 +1,23 @@
[tox]
downloadcache = {toxworkdir}/cache/
envlist =
py27,
py34,
py35,
py36,
py37,
py38,
py39,
[testenv]
basepython =
py26: python2.6
py27: python2.7
py34: python3.4
py35: python3.5
py36: python3.6
py37: python3.7
py38: python3.8
py39: python3.9
commands =
flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
py.test
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