Compare commits

...

150 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
48 changed files with 1270 additions and 981 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

View File

@ -5,11 +5,11 @@ name: Python package
on: on:
push: push:
branches: [ master, tproxy_mark_param ] branches: [ master ]
pull_request: pull_request:
branches: [ master, tproxy_mark_param ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
branches: [ tproxy_mark_param ] branches: [ master ]
jobs: jobs:
build: build:
@ -17,12 +17,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8, 3.9] python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2.4.0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.3.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

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,12 +1,9 @@
========== ==========
Change log Change log
========== ==========
All notable changes to this project will be documented in this file. The format Release notes now moved to https://github.com/sshuttle/sshuttle/releases/
is based on `Keep a Changelog`_ and this project
adheres to `Semantic Versioning`_.
.. _`Keep a Changelog`: http://keepachangelog.com/ These are the old release notes.
.. _`Semantic Versioning`: http://semver.org/
1.0.5 - 2020-12-29 1.0.5 - 2020-12-29
@ -133,7 +130,7 @@ Fixed
Added Added
~~~~~ ~~~~~
* doas support as replacmeent for sudo on OpenBSD. * doas support as replacement for sudo on OpenBSD.
* Added ChromeOS section to documentation (#262) * Added ChromeOS section to documentation (#262)
* Add --no-sudo-pythonpath option * Add --no-sudo-pythonpath option

View File

@ -46,6 +46,14 @@ Obtaining sshuttle
dnf install sshuttle dnf install sshuttle
- openSUSE::
zypper in sshuttle
- Gentoo::
emerge -av net-proxy/sshuttle
- NixOS:: - NixOS::
nix-env -iA nixos.sshuttle nix-env -iA nixos.sshuttle
@ -67,6 +75,11 @@ Obtaining sshuttle
# pkg # pkg
pkg install py36-sshuttle 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. It is also possible to install into a virtualenv as a non-root user.
- From PyPI:: - From PyPI::

View File

@ -50,6 +50,14 @@ if [ "$FILE_NAME" == "" ]; then
exit 1 exit 1
fi 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 # Make a temp file to hold the sudoers config
umask 077 umask 077
TEMP_FILE=$(mktemp) TEMP_FILE=$(mktemp)
@ -62,9 +70,9 @@ visudo_code=$?
rm "$TEMP_FILE" rm "$TEMP_FILE"
if [ $visudo_code -eq 0 ]; then if [ $visudo_code -eq 0 ]; then
echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME" echo "$CONTENT" > "$FILE_NAME"
chmod 0440 "/etc/sudoers.d/$FILE_NAME" chmod 0440 "$FILE_NAME"
echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!" echo "The sudoers file $FILE_NAME has been successfully created!"
exit 0 exit 0
else else

View File

@ -9,4 +9,3 @@ stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and
it just works, as do xterms and ssvncviewer etc. it just works, as do xterms and ssvncviewer etc.
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide 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 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 it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
safe. safe.

View File

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

View File

@ -5,7 +5,7 @@ Installation
pip install sshuttle pip install sshuttle
- Debain package manager:: - Debian package manager::
sudo apt install sshuttle sudo apt install sshuttle

View File

@ -4,14 +4,14 @@ sshuttle
Synopsis Synopsis
-------- --------
**sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\> **sshuttle** [*options*] **-r** *[username@]sshserver[:port]* \<*subnets* ...\>
Description Description
----------- -----------
:program:`sshuttle` allows you to create a VPN connection from your :program:`sshuttle` allows you to create a VPN connection from your
machine to any remote server that you can connect to via machine to any remote server that you can connect to via ssh, as long
ssh, as long as that server has python 3.6 or higher. as that server has a sufficiently new Python installation.
To work, you must have root access on the local machine, To work, you must have root access on the local machine,
but you can have a normal account on the server. but you can have a normal account on the server.
@ -31,22 +31,23 @@ Options
.. option:: <subnets> .. option:: <subnets>
A list of subnets to route over the VPN, in the form A list of subnets to route over the VPN, in the form
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4), 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 1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 netmask).
netmask), and 0/0 ('just route everything through the Specify subnets 0/0 to match all IPv4 addresses and ::/0 to match
VPN'). Any of the previous examples are also valid if you append all IPv6 addresses. Any of the previous examples are also valid if
a port or a port range, so 1.2.3.4:8000 will only tunnel traffic you append a port or a port range, so 1.2.3.4:8000 will only
that has as the destination port 8000 of 1.2.3.4 and tunnel traffic that has as the destination port 8000 of 1.2.3.4
1.2.3.0/24:8000-9000 will tunnel traffic going to any port between and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port
8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet. between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24
A hostname can be provided instead of an IP address. If the subnet. A hostname can be provided instead of an IP address. If
hostname resolves to multiple IPs, all of the IPs are included. the hostname resolves to multiple IPs, all of the IPs are
If a width is provided with a hostname that the width is applied included. If a width is provided with a hostname, the width is
to all of the hostnames IPs (if they are all either IPv4 or IPv6). applied to all of the hostnames IPs (if they are all either IPv4
Widths cannot be supplied to hostnames that resolve to both IPv4 or IPv6). Widths cannot be supplied to hostnames that resolve to
and IPv6. Valid examples are example.com, example.com:8000, both IPv4 and IPv6. Valid examples are example.com,
example.com/24, example.com/24:8000 and example.com:8000-9000. example.com:8000, example.com/24, example.com/24:8000 and
example.com:8000-9000.
.. option:: --method <auto|nat|nft|tproxy|pf|ipfw> .. option:: --method <auto|nat|nft|tproxy|pf|ipfw>
@ -88,6 +89,13 @@ Options
few subnets over the VPN, you probably would prefer to few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else. 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 .. option:: -N, --auto-nets
In addition to the subnets provided on the command In addition to the subnets provided on the command
@ -141,7 +149,10 @@ Options
The remote hostname and optional username and ssh The remote hostname and optional username and ssh
port number to use for connecting to the remote server. port number to use for connecting to the remote server.
For example, example.com, testuser@example.com, For example, example.com, testuser@example.com,
testuser@example.com:2222, or example.com:2244. testuser@example.com:2222, or example.com:2244. This
hostname is passed to ssh, so it will recognize any
aliases and settings you may have configured in
~/.ssh/config.
.. option:: -x <subnet>, --exclude=<subnet> .. option:: -x <subnet>, --exclude=<subnet>
@ -174,7 +185,7 @@ Options
A comma-separated list of hostnames to use to A comma-separated list of hostnames to use to
initialize the :option:`--auto-hosts` scan algorithm. 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 for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start if you use this option to give it a few names to start
from. from.
@ -274,9 +285,10 @@ Options
Set the file name for the sudoers.d file to be added. Default is Set the file name for the sudoers.d file to be added. Default is
"sshuttle_auto". Only works with --sudoers. "sshuttle_auto". Only works with --sudoers.
.. option:: -t, --tmark .. option:: -t <mark>, --tmark=<mark>
Transproxy optional traffic mark with provided MARK value. An option used by the tproxy method: Use the specified traffic
mark. The mark must be a hexadecimal value. Defaults to 0x01.
.. option:: --version .. option:: --version
@ -305,54 +317,107 @@ Arguments read from a file must be one per line, as shown below::
--option2 --option2
value2 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 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. $ sshuttle -r example.com -x example.com:22 0/0
Listening on ('0.0.0.0', 12300).
Starting sshuttle proxy (version ...).
[local sudo] Password: [local sudo] Password:
firewall manager ready. fw: Starting firewall with Python version 3.9.5
c : connecting to server... fw: ready method name nat.
s: available routes: c : IPv6 disabled since it isn't supported by method nat.
s: 192.168.42.0/24 c : Method: nat
c : connected. c : IPv4: on
firewall manager: starting transproxy. c : IPv6: off (not available with nat method)
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139. c : UDP : off (not available with nat method)
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443. c : DNS : off (available)
...etc... 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 ^C
firewall manager: undoing changes.
KeyboardInterrupt
c : Keyboard interrupt: exiting. c : Keyboard interrupt: exiting.
c : SW#8:192.168.42.121:47523: deleting c : SW'unknown':Mux#1: deleting (1 remain)
c : SW#6:192.168.42.106:50035: deleting 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:: and subnet guessing::
$ sshuttle -vNHr example.org $ sshuttle -vNHr example.com -x example.com:22
Starting sshuttle proxy (version ...).
Starting sshuttle proxy. [local sudo] Password:
Listening on ('0.0.0.0', 12300). fw: Starting firewall with Python version 3.9.5
firewall manager ready. fw: ready method name nat.
c : connecting to server... 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: available routes:
s: 77.141.99.0/24 s: 77.141.99.0/24
c : connected. fw: setting up.
c : seed_hosts: [] fw: iptables -w -t nat -N sshuttle-12300
firewall manager: starting transproxy. fw: iptables -w -t nat -F sshuttle-12300
hostwatch: Found: testbox1: 1.2.3.4 ...
hostwatch: Found: mytest2: 5.6.7.8
hostwatch: Found: domaincontroller: 99.1.2.3
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22. c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
^C ^C
firewall manager: undoing changes.
c : Keyboard interrupt: exiting. 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:: Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
@ -376,9 +441,7 @@ Example configuration file::
Discussion Discussion
---------- ----------
When it starts, :program:`sshuttle` creates an ssh session to the When it starts, :program:`sshuttle` creates an ssh session to the
server specified by the ``-r`` option. If ``-r`` is omitted, server specified by the ``-r`` option.
it will start both its client and server locally, which is
sometimes useful for testing.
After connecting to the remote server, :program:`sshuttle` uploads its After connecting to the remote server, :program:`sshuttle` uploads its
(python) source code to the remote end and executes it (python) source code to the remote end and executes it

View File

@ -15,10 +15,12 @@ Supports:
* IPv4 TCP * IPv4 TCP
* IPv4 DNS * IPv4 DNS
* IPv6 TCP
* IPv6 DNS
Requires: Requires:
* iptables DNAT, REDIRECT, and ttl modules. * iptables DNAT and REDIRECT modules. ip6tables for IPv6.
Linux with nft method Linux with nft method
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
@ -38,11 +40,11 @@ Linux with TPROXY method
Supports: Supports:
* IPv4 TCP * IPv4 TCP
* IPv4 UDP (requires ``recvmsg`` - see below) * IPv4 UDP
* IPv6 DNS (requires ``recvmsg`` - see below) * IPv4 DNS
* IPv6 TCP * IPv6 TCP
* IPv6 UDP (requires ``recvmsg`` - see below) * IPv6 UDP
* IPv6 DNS (requires ``recvmsg`` - see below) * IPv6 DNS
MacOS / FreeBSD / OpenBSD / pfSense MacOS / FreeBSD / OpenBSD / pfSense

View File

@ -1,6 +1,6 @@
TPROXY 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: There are some things you need to consider for TPROXY to work:
@ -12,24 +12,20 @@ There are some things you need to consider for TPROXY to work:
ip -6 route add local default dev lo table 100 ip -6 route add local default dev lo table 100
ip -6 rule add fwmark {TMARK} lookup 100 ip -6 rule add fwmark {TMARK} lookup 100
where {TMARK} is the identifier mark passed with -t or --tmark flag (default value is 1). 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 - 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. routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
- The client needs to be run as root. e.g.:: - The client needs to be run as root. e.g.::
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ... sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
- You may need to exclude the IP address of the server you are connecting to. - You may need to exclude the IP address of the server you are connecting to.
Otherwise sshuttle may attempt to intercept the ssh packets, which will not Otherwise sshuttle may attempt to intercept the ssh packets, which will not
work. Use the ``--exclude`` parameter for this. 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. - You need the ``--method=tproxy`` parameter, as above.
- The routes for the outgoing packets must already exist. For example, if your - 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 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. program that was the best of all worlds. And here we are, at last.
You're welcome. You're welcome.

View File

@ -11,6 +11,10 @@ Forward all traffic::
sshuttle -r username@sshserver 0.0.0.0/0 sshuttle -r username@sshserver 0.0.0.0/0
- Use the :option:`sshuttle -r` parameter to specify a remote server. - 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 - By default sshuttle will automatically choose a method to use. Override with
the :option:`sshuttle --method` parameter. the :option:`sshuttle --method` parameter.
@ -95,7 +99,7 @@ access to sshuttle. The default is `sshuttle_auto`::
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
You can also see what configuration will be added to your system without You can also see what configuration will be added to your system without
modifying anything. This can be helpfull is the auto feature does not work, or 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`. you want more control. This option also works with `--sudoers-username`.
`--sudoers-filename` has no effect with this option:: `--sudoers-filename` has no effect with this option::

View File

@ -16,4 +16,4 @@ Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can
configure that in Vagrant), we can then ask Windows to route all its traffic configure that in Vagrant), we can then ask Windows to route all its traffic
via the VM by running the following in cmd.exe with admin right:: via the VM by running the following in cmd.exe with admin right::
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200 route add 0.0.0.0 mask 0.0.0.0 192.168.1.200

View File

@ -1,7 +1,5 @@
-r requirements.txt -r requirements.txt
attrs==20.3.0 pytest==6.2.5
pytest==6.2.1 pytest-cov==3.0.0
pytest-cov==2.10.1 flake8==4.0.1
mock==4.0.3 pyflakes==2.4.0
flake8==3.8.4
pyflakes==2.2.0

View File

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

4
run
View File

@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -e set -e
export PYTHONPATH="$(dirname $0):$PYTHONPATH" export PYTHONPATH="$(dirname "$0"):$PYTHONPATH"
export PATH="$(dirname $0)/bin:$PATH" export PATH="$(dirname "$0")/bin:$PATH"
python_best_version() { python_best_version() {
if [ -x "$(command -v python3)" ] && if [ -x "$(command -v python3)" ] &&

View File

@ -63,13 +63,11 @@ setup(
}, },
python_requires='>=3.6', python_requires='>=3.6',
install_requires=[ install_requires=[
'psutil',
], ],
tests_require=[ tests_require=[
'pytest', 'pytest',
'pytest-cov', 'pytest-cov',
'pytest-runner', 'pytest-runner',
'mock',
'flake8', 'flake8',
], ],
keywords="ssh vpn", keywords="ssh vpn",

View File

@ -1,8 +1,12 @@
import sys import sys
import zlib import zlib
import types import types
import platform
verbosity = verbosity # noqa: F821 must be a previously defined global 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() z = zlib.decompressobj()
while 1: while 1:
name = sys.stdin.readline().strip() name = sys.stdin.readline().strip()
@ -14,7 +18,7 @@ while 1:
name = name.decode("ASCII") name = name.decode("ASCII")
nbytes = int(sys.stdin.readline()) nbytes = int(sys.stdin.readline())
if verbosity >= 2: if verbosity >= 2:
sys.stderr.write(' s: assembling %r (%d bytes)\n' sys.stderr.write(' s: assembling %r (%d bytes)\r\n'
% (name, nbytes)) % (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes)) content = z.decompress(sys.stdin.read(nbytes))
@ -40,5 +44,6 @@ sshuttle.helpers.verbose = verbosity
import sshuttle.cmdline_options as options # noqa: E402 import sshuttle.cmdline_options as options # noqa: E402
from sshuttle.server import main # noqa: E402 from sshuttle.server import main # noqa: E402
main(options.latency_control, options.auto_hosts, options.to_nameserver, main(options.latency_control, options.latency_buffer_size,
options.auto_hosts, options.to_nameserver,
options.auto_nets) options.auto_nets)

View File

@ -6,7 +6,6 @@ import subprocess as ssubprocess
import os import os
import sys import sys
import platform import platform
import psutil
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
import sshuttle.ssnet as ssnet import sshuttle.ssnet as ssnet
@ -23,28 +22,17 @@ try:
except ImportError: except ImportError:
getpwnam = None getpwnam = None
try: import socket
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
except ImportError:
import socket
_extra_fd = os.open(os.devnull, os.O_RDONLY) _extra_fd = os.open(os.devnull, os.O_RDONLY)
def got_signal(signum, frame): def got_signal(signum, frame):
log('exiting on signal %d\n' % signum) log('exiting on signal %d' % signum)
sys.exit(1) sys.exit(1)
# Filename of the pidfile created by the sshuttle client.
_pidname = None _pidname = None
@ -57,7 +45,7 @@ def check_daemon(pidfile):
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
return # no pidfile, ok return # no pidfile, ok
else: else:
raise Fatal("c : can't read %s: %s" % (_pidname, e)) raise Fatal("can't read %s: %s" % (_pidname, e))
if not oldpid: if not oldpid:
os.unlink(_pidname) os.unlink(_pidname)
return # invalid pidfile, ok return # invalid pidfile, ok
@ -80,13 +68,25 @@ def check_daemon(pidfile):
def daemonize(): 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(): if os.fork():
os._exit(0) os._exit(0)
os.setsid() os.setsid()
if os.fork(): if os.fork():
os._exit(0) os._exit(0)
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666) # Write pid to the pidfile.
try: try:
os.write(outfd, b'%d\n' % os.getpid()) os.write(outfd, b'%d\n' % os.getpid())
finally: finally:
@ -152,7 +152,7 @@ class MultiListener:
try: try:
self.v4.listen(backlog) self.v4.listen(backlog)
except socket.error as e: 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. # in this case the v6 socket will receive v4 too.
if e.errno == errno.EADDRINUSE and self.v6: if e.errno == errno.EADDRINUSE and self.v6:
self.v4 = None self.v4 = None
@ -177,19 +177,19 @@ class MultiListener:
assert(self.bind_called) assert(self.bind_called)
if self.v6: if self.v6:
listenip = self.v6.getsockname() listenip = self.v6.getsockname()
debug1('%s listening on %r.\n' % (what, listenip)) debug1('%s listening on %r.' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v6)) debug2('%s listening with %r.' % (what, self.v6))
if self.v4: if self.v4:
listenip = self.v4.getsockname() listenip = self.v4.getsockname()
debug1('%s listening on %r.\n' % (what, listenip)) debug1('%s listening on %r.' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v4)) debug2('%s listening with %r.' % (what, self.v4))
class FirewallClient: class FirewallClient:
def __init__(self, method_name, sudo_pythonpath): def __init__(self, method_name, sudo_pythonpath):
self.auto_nets = [] self.auto_nets = []
python_path = os.path.dirname(os.path.dirname(__file__))
argvbase = ([sys.executable, sys.argv[0]] + argvbase = ([sys.executable, sys.argv[0]] +
['-v'] * (helpers.verbose or 0) + ['-v'] * (helpers.verbose or 0) +
['--method', method_name] + ['--method', method_name] +
@ -197,54 +197,101 @@ class FirewallClient:
if ssyslog._p: if ssyslog._p:
argvbase += ['--syslog'] argvbase += ['--syslog']
# Determine how to prefix the command in order to elevate privileges. # A list of commands that we can try to run to start the firewall.
if platform.platform().startswith('OpenBSD'): argv_tries = []
elev_prefix = ['doas'] # OpenBSD uses built in `doas`
if os.getuid() == 0: # No need to elevate privileges
argv_tries.append(argvbase)
else: else:
elev_prefix = ['sudo', '-p', '[local sudo] Password: '] # 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
# Look for binary and switch to absolute path if we can find # For clarity, try to replace executable name with the
# it. # full path.
path = which(elev_prefix[0]) doas_path = which("doas")
if path: if doas_path:
elev_prefix[0] = path doas_cmd[0] = doas_path
sudo_path = which("sudo")
if sudo_path:
sudo_cmd[0] = sudo_path
if sudo_pythonpath: # sudo_pythonpath indicates if we should set the
elev_prefix += ['/usr/bin/env', # PYTHONPATH environment variable when elevating
'PYTHONPATH=%s' % python_path] # privileges. This can be adjusted with the
argv_tries = [elev_prefix + argvbase, argvbase] # --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
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would, # If we can find doas and not sudo or if we are on
# because stupid Linux 'su' requires that stdin be attached to a tty. # OpenBSD, try using doas first.
# Instead, attach a *bidirectional* socket to its stdout, and use if (doas_path and not sudo_path) or \
# that for talking in both directions. platform.platform().startswith('OpenBSD'):
(s1, s2) = socket.socketpair() argv_tries = [doas_cmd, sudo_cmd, argvbase]
else:
argv_tries = [sudo_cmd, doas_cmd, argvbase]
def setup(): # Try all commands in argv_tries in order. If a command
# run in the child process # produces an error, try the next one. If command is
s2.close() # successful, set 'success' variable and break.
if os.getuid() == 0: success = False
argv_tries = argv_tries[-1:] # last entry only
for argv in argv_tries: 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()
try: try:
if argv[0] == 'su': debug1("Starting firewall manager with command: %r" % argv)
sys.stderr.write('[local su] ')
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup) self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
# No env: Talking to `FirewallClient.start`, which has no i18n. # No env: Talking to `FirewallClient.start`, which has no i18n.
break
except OSError as e: except OSError as e:
log('Spawning firewall manager: %r\n' % argv) # This exception will occur if the program isn't
raise Fatal(e) # present or isn't executable.
self.argv = argv debug1('Unable to start firewall manager. Popen failed. '
s1.close() 'Command=%r Exception=%s' % (argv, e))
self.pfile = s2.makefile('rwb') continue
line = self.pfile.readline()
self.check() self.argv = argv
if line[0:5] != b'READY': s1.close()
raise Fatal('%r expected READY, got %r' % (self.argv, line)) self.pfile = s2.makefile('rwb')
method_name = line[6:-1] line = self.pfile.readline()
self.method = get_method(method_name.decode("ASCII"))
self.method.set_firewall(self) 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':
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, def setup(self, subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
@ -297,7 +344,8 @@ class FirewallClient:
else: else:
user = b'%d' % self.user 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() self.pfile.flush()
line = self.pfile.readline() line = self.pfile.readline()
@ -326,23 +374,23 @@ def expire_connections(now, mux):
remove = [] remove = []
for chan, timeout in dnsreqs.items(): for chan, timeout in dnsreqs.items():
if timeout < now: if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan) debug3('expiring dnsreqs channel=%d' % chan)
remove.append(chan) remove.append(chan)
del mux.channels[chan] del mux.channels[chan]
for chan in remove: for chan in remove:
del dnsreqs[chan] del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) debug3('Remaining DNS requests: %d' % len(dnsreqs))
remove = [] remove = []
for peer, (chan, timeout) in udp_by_src.items(): for peer, (chan, timeout) in udp_by_src.items():
if timeout < now: 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'') mux.send(chan, ssnet.CMD_UDP_CLOSE, b'')
remove.append(peer) remove.append(peer)
del mux.channels[chan] del mux.channels[chan]
for peer in remove: for peer in remove:
del udp_by_src[peer] 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): def onaccept_tcp(listener, method, mux, handlers):
@ -351,7 +399,7 @@ def onaccept_tcp(listener, method, mux, handlers):
sock, srcip = listener.accept() sock, srcip = listener.accept()
except socket.error as e: except socket.error as e:
if e.args[0] in [errno.EMFILE, errno.ENFILE]: 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 # free up an fd so we can eat the connection
os.close(_extra_fd) os.close(_extra_fd)
try: try:
@ -364,15 +412,15 @@ def onaccept_tcp(listener, method, mux, handlers):
raise raise
dstip = method.get_tcp_dstip(sock) 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])) dstip[0], dstip[1]))
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family): 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() sock.close()
return return
chan = mux.next_channel() chan = mux.next_channel()
if not chan: if not chan:
log('warning: too many open channels. Discarded connection.\n') log('warning: too many open channels. Discarded connection.')
sock.close() sock.close()
return return
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' % mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
@ -385,7 +433,7 @@ def onaccept_tcp(listener, method, mux, handlers):
def udp_done(chan, data, method, sock, dstip): def udp_done(chan, data, method, sock, dstip):
(src, srcport, data) = data.split(b",", 2) (src, srcport, data) = data.split(b",", 2)
srcip = (src, int(srcport)) 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) method.send_udp(sock, srcip, dstip, data)
@ -395,7 +443,7 @@ def onaccept_udp(listener, method, mux, handlers):
if t is None: if t is None:
return return
srcip, dstip, data = t 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: if srcip in udp_by_src:
chan, _ = udp_by_src[srcip] chan, _ = udp_by_src[srcip]
else: else:
@ -412,7 +460,7 @@ def onaccept_udp(listener, method, mux, handlers):
def dns_done(chan, data, method, sock, srcip, dstip, mux): 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 mux.channels[chan]
del dnsreqs[chan] del dnsreqs[chan]
method.send_udp(sock, srcip, dstip, data) method.send_udp(sock, srcip, dstip, data)
@ -427,9 +475,9 @@ def ondns(listener, method, mux, handlers):
# dstip is None if we are using a method where we can't determine # 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. # the destination IP of the DNS request that we captured from the client.
if dstip is None: if dstip is None:
debug1('DNS request from %r: %d bytes\n' % (srcip, len(data))) debug1('DNS request from %r: %d bytes' % (srcip, len(data)))
else: else:
debug1('DNS request from %r to %r: %d bytes\n' % debug1('DNS request from %r to %r: %d bytes' %
(srcip, dstip, len(data))) (srcip, dstip, len(data)))
chan = mux.next_channel() chan = mux.next_channel()
dnsreqs[chan] = now + 30 dnsreqs[chan] = now + 30
@ -440,30 +488,31 @@ def ondns(listener, method, mux, handlers):
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, python, latency_control, latency_buffer_size,
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon, dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
to_nameserver): to_nameserver):
helpers.logprefix = 'c : ' helpers.logprefix = 'c : '
debug1('Starting client with Python version %s\n' debug1('Starting client with Python version %s'
% platform.python_version()) % platform.python_version())
method = fw.method method = fw.method
handlers = [] handlers = []
debug1('Connecting to server...\n') debug1('Connecting to server...')
try: try:
(serverproc, serversock) = ssh.connect( (serverproc, serversock) = ssh.connect(
ssh_cmd, remotename, python, ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin, stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control, options=dict(latency_control=latency_control,
latency_buffer_size=latency_buffer_size,
auto_hosts=auto_hosts, auto_hosts=auto_hosts,
to_nameserver=to_nameserver, to_nameserver=to_nameserver,
auto_nets=auto_nets)) auto_nets=auto_nets))
except socket.error as e: except socket.error as e:
if e.args[0] == errno.EPIPE: if e.args[0] == errno.EPIPE:
raise Fatal("c : failed to establish ssh session (1)") raise Fatal("failed to establish ssh session (1)")
else: else:
raise raise
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb")) mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
@ -481,22 +530,99 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
initstring = serversock.recv(len(expected)) initstring = serversock.recv(len(expected))
except socket.error as e: except socket.error as e:
if e.args[0] == errno.ECONNRESET: if e.args[0] == errno.ECONNRESET:
raise Fatal("c : failed to establish ssh session (2)") raise Fatal("failed to establish ssh session (2)")
else: else:
raise raise
# Returns None if process is still running (or returns exit code)
rv = serverproc.poll() rv = serverproc.poll()
if rv: if rv is not None:
raise Fatal('c : server died with error code %d' % rv) 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: if initstring != expected:
raise Fatal('c : expected server init string %r; got %r' raise Fatal('expected server init string %r; got %r'
% (expected, initstring)) % (expected, initstring))
log('Connected to server.\n') log('Connected to server.')
sys.stdout.flush() sys.stdout.flush()
if daemon: if daemon:
daemonize() daemonize()
log('daemonizing (%s).\n' % _pidname) log('daemonizing (%s).' % _pidname)
def onroutes(routestr): def onroutes(routestr):
if auto_nets: if auto_nets:
@ -508,11 +634,11 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
width = int(width) width = int(width)
ip = ip.decode("ASCII") ip = ip.decode("ASCII")
if family == socket.AF_INET6 and tcp_listener.v6 is None: 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: 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: 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)) fw.auto_nets.append((family, ip, width, 0, 0))
# we definitely want to do this *after* starting ssh, or we might end # we definitely want to do this *after* starting ssh, or we might end
@ -532,7 +658,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected')) sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
def onhostlist(hostlist): def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist) debug2('got host list: %r' % hostlist)
for line in hostlist.strip().split(): for line in hostlist.strip().split():
if line: if line:
name, ip = line.split(b',', 1) name, ip = line.split(b',', 1)
@ -548,7 +674,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
dns_listener.add_handler(handlers, ondns, method, mux) dns_listener.add_handler(handlers, ondns, method, mux)
if seed_hosts is not None: 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))) mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
def check_ssh_alive(): def check_ssh_alive():
@ -556,7 +682,9 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
# poll() won't tell us when process exited since the # poll() won't tell us when process exited since the
# process is no longer our child (it returns 0 all the # process is no longer our child (it returns 0 all the
# time). # time).
if not psutil.pid_exists(serverproc.pid): try:
os.kill(serverproc.pid, 0)
except OSError:
raise Fatal('ssh connection to server (pid %d) exited.' % raise Fatal('ssh connection to server (pid %d) exited.' %
serverproc.pid) serverproc.pid)
else: else:
@ -574,40 +702,47 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
def main(listenip_v6, listenip_v4, def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, nslist, ssh_cmd, remotename, python, latency_control,
latency_buffer_size, dns, nslist,
method_name, seed_hosts, auto_hosts, auto_nets, method_name, seed_hosts, auto_hosts, auto_nets,
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile, subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
user, sudo_pythonpath, tmark): user, sudo_pythonpath, tmark):
if not remotename: if not remotename:
print("WARNING: You must specify -r/--remote to securely route " raise Fatal("You must use -r/--remote to specify a remote "
"traffic to a remote machine. Running without -r/--remote " "host to route traffic through.")
"is only recommended for testing.")
if daemon: if daemon:
try: try:
check_daemon(pidfile) check_daemon(pidfile)
except Fatal as e: except Fatal as e:
log("%s\n" % e) log("%s" % e)
return 5 return 5
debug1('Starting sshuttle proxy (version %s).\n' % __version__) debug1('Starting sshuttle proxy (version %s).' % __version__)
helpers.logprefix = 'c : ' helpers.logprefix = 'c : '
fw = FirewallClient(method_name, sudo_pythonpath) fw = FirewallClient(method_name, sudo_pythonpath)
# If --dns is used, store the IP addresses that the client # nslist is the list of name severs to intercept. If --dns is
# normally uses for DNS lookups in nslist. The firewall needs to # used, we add all DNS servers in resolv.conf. Otherwise, the list
# redirect packets outgoing to this server to the remote host # 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. # instead.
if dns: if dns:
nslist += resolvconf_nameservers(True) 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: if to_nameserver is not None:
to_nameserver = "%s@%s" % tuple(to_nameserver[1:]) to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
else: else: # if we are not intercepting DNS traffic
# option doesn't make sense if we aren't proxying dns # ...and the user specified a server to send DNS traffic to.
if to_nameserver and len(to_nameserver) > 0: if to_nameserver and len(to_nameserver) > 0:
print("WARNING: --to-ns option is ignored because --dns was not " print("WARNING: --to-ns option is ignored unless "
"used.") "--dns or --ns-hosts is used.")
to_nameserver = None to_nameserver = None
# Get family specific subnet lists. Also, the user may not specify # Get family specific subnet lists. Also, the user may not specify
@ -643,14 +778,14 @@ def main(listenip_v6, listenip_v4,
# "auto" when listen address is unspecified. # "auto" when listen address is unspecified.
# The user specified address if provided by user # The user specified address if provided by user
if listenip_v6 is None: if listenip_v6 is None:
debug1("IPv6 disabled by --disable-ipv6\n") debug1("IPv6 disabled by --disable-ipv6")
if listenip_v6 == "auto": if listenip_v6 == "auto":
if avail.ipv6: if avail.ipv6:
debug1("IPv6 enabled: Using default IPv6 listen address ::1\n") debug1("IPv6 enabled: Using default IPv6 listen address ::1")
listenip_v6 = ('::1', 0) listenip_v6 = ('::1', 0)
else: else:
debug1("IPv6 disabled since it isn't supported by method " debug1("IPv6 disabled since it isn't supported by method "
"%s.\n" % fw.method.name) "%s." % fw.method.name)
listenip_v6 = None listenip_v6 = None
# Make final decision about enabling IPv6: # Make final decision about enabling IPv6:
@ -722,9 +857,9 @@ def main(listenip_v6, listenip_v4,
msg += "(available)" msg += "(available)"
else: else:
msg += "(not available with %s method)" % fw.method.name msg += "(not available with %s method)" % fw.method.name
debug1(msg + "\n") debug1(msg)
debug1("Method: %s\n" % fw.method.name) debug1("Method: %s" % fw.method.name)
feature_status("IPv4", required.ipv4, avail.ipv4) feature_status("IPv4", required.ipv4, avail.ipv4)
feature_status("IPv6", required.ipv6, avail.ipv6) feature_status("IPv6", required.ipv6, avail.ipv6)
feature_status("UDP ", required.udp, avail.udp) feature_status("UDP ", required.udp, avail.udp)
@ -744,20 +879,20 @@ def main(listenip_v6, listenip_v4,
# because we do that below when we have identified the ports to # because we do that below when we have identified the ports to
# listen on. # listen on.
debug1("Subnets to forward through remote host (type, IP, cidr mask " debug1("Subnets to forward through remote host (type, IP, cidr mask "
"width, startPort, endPort):\n") "width, startPort, endPort):")
for i in subnets_include: for i in subnets_include:
debug1(" "+str(i)+"\n") debug1(" "+str(i))
if auto_nets: if auto_nets:
debug1("NOTE: Additional subnets to forward may be added below by " debug1("NOTE: Additional subnets to forward may be added below by "
"--auto-nets.\n") "--auto-nets.")
debug1("Subnets to exclude from forwarding:\n") debug1("Subnets to exclude from forwarding:")
for i in subnets_exclude: for i in subnets_exclude:
debug1(" "+str(i)+"\n") debug1(" "+str(i))
if required.dns: if required.dns:
debug1("DNS requests normally directed at these servers will be " debug1("DNS requests normally directed at these servers will be "
"redirected to remote:\n") "redirected to remote:")
for i in nslist: for i in nslist:
debug1(" "+str(i)+"\n") debug1(" "+str(i))
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]: if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
# if both ports given, no need to search for a spare port # if both ports given, no need to search for a spare port
@ -775,7 +910,7 @@ def main(listenip_v6, listenip_v4,
redirectport_v4 = 0 redirectport_v4 = 0
bound = False bound = False
for port in ports: for port in ports:
debug2('Trying to bind redirector on port %d\n' % port) debug2('Trying to bind redirector on port %d' % port)
tcp_listener = MultiListener() tcp_listener = MultiListener()
if required.udp: if required.udp:
@ -830,7 +965,7 @@ def main(listenip_v6, listenip_v4,
# search for spare port for DNS # search for spare port for DNS
ports = range(12300, 9000, -1) ports = range(12300, 9000, -1)
for port in ports: for port in ports:
debug2('Trying to bind DNS redirector on port %d\n' % port) debug2('Trying to bind DNS redirector on port %d' % port)
if port in used_ports: if port in used_ports:
continue continue
@ -908,8 +1043,9 @@ def main(listenip_v6, listenip_v4,
# start the client process # start the client process
try: try:
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, dns_listener, python, latency_control, latency_buffer_size,
seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver) dns_listener, seed_hosts, auto_hosts, auto_nets,
daemon, to_nameserver)
finally: finally:
try: try:
if daemon: if daemon:

View File

@ -17,11 +17,11 @@ def main():
if opt.sudoers or opt.sudoers_no_modify: if opt.sudoers or opt.sudoers_no_modify:
if platform.platform().startswith('OpenBSD'): if platform.platform().startswith('OpenBSD'):
log('Automatic sudoers does not work on BSD') log('Automatic sudoers does not work on BSD')
exit(1) return 1
if not opt.sudoers_filename: if not opt.sudoers_filename:
log('--sudoers-file must be set or omited.') log('--sudoers-file must be set or omitted.')
exit(1) return 1
sudoers( sudoers(
user_name=opt.sudoers_user, user_name=opt.sudoers_user,
@ -85,6 +85,13 @@ def main():
ipport_v4 = "auto" ipport_v4 = "auto"
# parse_ipport6('[::1]:0') # parse_ipport6('[::1]:0')
ipport_v6 = "auto" if not opt.disable_ipv6 else None 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: if opt.syslog:
ssyslog.start_syslog() ssyslog.start_syslog()
ssyslog.close_stdin() ssyslog.close_stdin()
@ -95,6 +102,7 @@ def main():
remotename, remotename,
opt.python, opt.python,
opt.latency_control, opt.latency_control,
opt.latency_buffer_size,
opt.dns, opt.dns,
nslist, nslist,
opt.method, opt.method,
@ -117,9 +125,9 @@ def main():
return return_code return return_code
except Fatal as e: except Fatal as e:
log('fatal: %s\n' % e) log('fatal: %s' % e)
return 99 return 99
except KeyboardInterrupt: except KeyboardInterrupt:
log('\n') log('\n')
log('Keyboard interrupt: exiting.\n') log('Keyboard interrupt: exiting.')
return 1 return 1

View File

@ -5,13 +5,15 @@ import sys
import os import os
import platform import platform
import traceback import traceback
import subprocess as ssubprocess
import sshuttle.ssyslog as ssyslog import sshuttle.ssyslog as ssyslog
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
from sshuttle.helpers import debug1, debug2, Fatal from sshuttle.helpers import log, debug1, debug2, Fatal
from sshuttle.methods import get_auto_method, get_method from sshuttle.methods import get_auto_method, get_method
HOSTSFILE = '/etc/hosts' HOSTSFILE = '/etc/hosts'
sshuttle_pid = None
def rewrite_etc_hosts(hostmap, port): def rewrite_etc_hosts(hostmap, port):
@ -51,26 +53,51 @@ def rewrite_etc_hosts(hostmap, port):
def restore_etc_hosts(hostmap, port): def restore_etc_hosts(hostmap, port):
# Only restore if we added hosts to /etc/hosts previously. # Only restore if we added hosts to /etc/hosts previously.
if len(hostmap) > 0: if len(hostmap) > 0:
debug2('undoing /etc/hosts changes.\n') debug2('undoing /etc/hosts changes.')
rewrite_etc_hosts({}, port) 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 # Isolate function that needs to be replaced for tests
def setup_daemon(): def setup_daemon():
if os.getuid() != 0: if os.getuid() != 0:
raise Fatal('fw: ' raise Fatal('You must be root (or enable su/sudo) to set the firewall')
'You must be root (or enable su/sudo) to set the firewall')
# don't disappear if our controlling terminal or stdout/stderr # don't disappear if our controlling terminal or stdout/stderr
# disappears; we still have to clean up. # disappears; we still have to clean up.
signal.signal(signal.SIGHUP, signal.SIG_IGN) signal.signal(signal.SIGHUP, signal.SIG_IGN)
signal.signal(signal.SIGPIPE, signal.SIG_IGN) signal.signal(signal.SIGPIPE, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_IGN) signal.signal(signal.SIGTERM, firewall_exit)
signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGINT, firewall_exit)
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies, # Calling setsid() here isn't strictly necessary. However, it forces
# I'll die automatically. # Ctrl+C to get sent to the main sshuttle process instead of to
os.setsid() # 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 # because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we # are both attached to stdout initially. Clone stdout into stdin so we
@ -90,19 +117,50 @@ def subnet_weight(s):
return (-s[-1] + (s[-2] or -65535), s[1], s[2]) 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 # This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules; # proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch. # otherwise we delete it, then make them from scratch.
# #
# This code is supposed to clean up after itself by deleting its rules on # 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 # 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. # are hopefully harmless.
def main(method_name, syslog): def main(method_name, syslog):
helpers.logprefix = 'fw: '
stdin, stdout = setup_daemon() stdin, stdout = setup_daemon()
hostmap = {} hostmap = {}
helpers.logprefix = 'fw: ' debug1('Starting firewall with Python version %s'
debug1('Starting firewall with Python version %s\n'
% platform.python_version()) % platform.python_version())
if method_name == "auto": if method_name == "auto":
@ -119,7 +177,7 @@ def main(method_name, syslog):
"Check that the appropriate programs are in your " "Check that the appropriate programs are in your "
"PATH." % method_name) "PATH." % method_name)
debug1('ready method name %s.\n' % method.name) debug1('ready method name %s.' % method.name)
stdout.write('READY %s\n' % method.name) stdout.write('READY %s\n' % method.name)
stdout.flush() stdout.flush()
@ -136,14 +194,14 @@ def main(method_name, syslog):
while 1: while 1:
line = stdin.readline(128) line = stdin.readline(128)
if not line: if not line:
raise Fatal('fw: expected route but got %r' % line) raise Fatal('expected route but got %r' % line)
elif line.startswith("NSLIST\n"): elif line.startswith("NSLIST\n"):
break break
try: try:
(family, width, exclude, ip, fport, lport) = \ (family, width, exclude, ip, fport, lport) = \
line.strip().split(',', 5) line.strip().split(',', 5)
except BaseException: except BaseException:
raise Fatal('fw: expected route or NSLIST but got %r' % line) raise Fatal('expected route or NSLIST but got %r' % line)
subnets.append(( subnets.append((
int(family), int(family),
int(width), int(width),
@ -151,31 +209,31 @@ def main(method_name, syslog):
ip, ip,
int(fport), int(fport),
int(lport))) int(lport)))
debug2('Got subnets: %r\n' % subnets) debug2('Got subnets: %r' % subnets)
nslist = [] nslist = []
if line != 'NSLIST\n': if line != 'NSLIST\n':
raise Fatal('fw: expected NSLIST but got %r' % line) raise Fatal('expected NSLIST but got %r' % line)
while 1: while 1:
line = stdin.readline(128) line = stdin.readline(128)
if not line: if not line:
raise Fatal('fw: expected nslist but got %r' % line) raise Fatal('expected nslist but got %r' % line)
elif line.startswith("PORTS "): elif line.startswith("PORTS "):
break break
try: try:
(family, ip) = line.strip().split(',', 1) (family, ip) = line.strip().split(',', 1)
except BaseException: except BaseException:
raise Fatal('fw: expected nslist or PORTS but got %r' % line) raise Fatal('expected nslist or PORTS but got %r' % line)
nslist.append((int(family), ip)) nslist.append((int(family), ip))
debug2('Got partial nslist: %r\n' % nslist) debug2('Got partial nslist: %r' % nslist)
debug2('Got nslist: %r\n' % nslist) debug2('Got nslist: %r' % nslist)
if not line.startswith('PORTS '): if not line.startswith('PORTS '):
raise Fatal('fw: expected PORTS but got %r' % line) raise Fatal('expected PORTS but got %r' % line)
_, _, ports = line.partition(" ") _, _, ports = line.partition(" ")
ports = ports.split(",") ports = ports.split(",")
if len(ports) != 4: if len(ports) != 4:
raise Fatal('fw: expected 4 ports but got %d' % len(ports)) raise Fatal('expected 4 ports but got %d' % len(ports))
port_v6 = int(ports[0]) port_v6 = int(ports[0])
port_v4 = int(ports[1]) port_v4 = int(ports[1])
dnsport_v6 = int(ports[2]) dnsport_v6 = int(ports[2])
@ -190,21 +248,24 @@ def main(method_name, syslog):
assert(dnsport_v4 >= 0) assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535) assert(dnsport_v4 <= 65535)
debug2('Got ports: %d,%d,%d,%d\n' debug2('Got ports: %d,%d,%d,%d'
% (port_v6, port_v4, dnsport_v6, dnsport_v4)) % (port_v6, port_v4, dnsport_v6, dnsport_v4))
line = stdin.readline(128) line = stdin.readline(128)
if not line: if not line:
raise Fatal('fw: expected GO but got %r' % line) raise Fatal('expected GO but got %r' % line)
elif not line.startswith("GO "): elif not line.startswith("GO "):
raise Fatal('fw: expected GO but got %r' % line) raise Fatal('expected GO but got %r' % line)
_, _, args = line.partition(" ") _, _, 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)) udp = bool(int(udp))
sshuttle_pid = int(sshuttle_pid)
if user == '-': if user == '-':
user = None user = None
debug2('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] 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] nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
@ -212,22 +273,23 @@ def main(method_name, syslog):
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
try: try:
debug1('setting up.\n') debug1('setting up.')
if subnets_v6 or nslist_v6: if subnets_v6 or nslist_v6:
debug2('setting up IPv6.\n') debug2('setting up IPv6.')
method.setup_firewall( method.setup_firewall(
port_v6, dnsport_v6, nslist_v6, port_v6, dnsport_v6, nslist_v6,
socket.AF_INET6, subnets_v6, udp, socket.AF_INET6, subnets_v6, udp,
user) user, tmark)
if subnets_v4 or nslist_v4: if subnets_v4 or nslist_v4:
debug2('setting up IPv4.\n') debug2('setting up IPv4.')
method.setup_firewall( method.setup_firewall(
port_v4, dnsport_v4, nslist_v4, port_v4, dnsport_v4, nslist_v4,
socket.AF_INET, subnets_v4, udp, socket.AF_INET, subnets_v4, udp,
user) user, tmark)
flush_systemd_dns_cache()
stdout.write('STARTED\n') stdout.write('STARTED\n')
try: try:
@ -245,40 +307,38 @@ def main(method_name, syslog):
if line.startswith('HOST '): if line.startswith('HOST '):
(name, ip) = line[5:].strip().split(',', 1) (name, ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip hostmap[name] = ip
debug2('setting up /etc/hosts.\n') debug2('setting up /etc/hosts.')
rewrite_etc_hosts(hostmap, port_v6 or port_v4) rewrite_etc_hosts(hostmap, port_v6 or port_v4)
elif line: elif line:
if not method.firewall_command(line): if not method.firewall_command(line):
raise Fatal('fw: expected command, got %r' % line) raise Fatal('expected command, got %r' % line)
else: else:
break break
finally: finally:
try: try:
debug1('undoing changes.\n') debug1('undoing changes.')
except BaseException: except BaseException:
debug2('An error occurred, ignoring it.') debug2('An error occurred, ignoring it.')
try: try:
if subnets_v6 or nslist_v6: if subnets_v6 or nslist_v6:
debug2('undoing IPv6 changes.\n') debug2('undoing IPv6 changes.')
method.restore_firewall(port_v6, socket.AF_INET6, udp, user) method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
except BaseException: except BaseException:
try: try:
debug1("Error trying to undo IPv6 firewall.\n") debug1("Error trying to undo IPv6 firewall.")
for line in traceback.format_exc().splitlines(): debug1(traceback.format_exc())
debug1("---> %s\n" % line)
except BaseException: except BaseException:
debug2('An error occurred, ignoring it.') debug2('An error occurred, ignoring it.')
try: try:
if subnets_v4 or nslist_v4: if subnets_v4 or nslist_v4:
debug2('undoing IPv4 changes.\n') debug2('undoing IPv4 changes.')
method.restore_firewall(port_v4, socket.AF_INET, udp, user) method.restore_firewall(port_v4, socket.AF_INET, udp, user)
except BaseException: except BaseException:
try: try:
debug1("Error trying to undo IPv4 firewall.\n") debug1("Error trying to undo IPv4 firewall.")
for line in traceback.format_exc().splitlines(): debug1(traceback.format_exc())
debug1("---> %s\n" % line)
except BaseException: except BaseException:
debug2('An error occurred, ignoring it.') debug2('An error occurred, ignoring it.')
@ -287,8 +347,16 @@ def main(method_name, syslog):
restore_etc_hosts(hostmap, port_v6 or port_v4) restore_etc_hosts(hostmap, port_v6 or port_v4)
except BaseException: except BaseException:
try: try:
debug1("Error trying to undo /etc/hosts changes.\n") debug1("Error trying to undo /etc/hosts changes.")
for line in traceback.format_exc().splitlines(): debug1(traceback.format_exc())
debug1("---> %s\n" % line)
except BaseException: except BaseException:
debug2('An error occurred, ignoring it.') 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

@ -15,14 +15,22 @@ def log(s):
global logprefix global logprefix
try: try:
sys.stdout.flush() sys.stdout.flush()
if s.find("\n") != -1: # Put newline at end of string if line doesn't have one.
prefix = logprefix if not s.endswith("\n"):
s = s.rstrip("\n") s = s+"\n"
for line in s.split("\n"):
sys.stderr.write(prefix + line + "\n") prefix = logprefix
prefix = "---> " s = s.rstrip("\n")
else: for line in s.split("\n"):
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() sys.stderr.flush()
except IOError: except IOError:
# this could happen if stderr gets forcibly disconnected, eg. because # this could happen if stderr gets forcibly disconnected, eg. because
@ -91,11 +99,11 @@ def resolvconf_nameservers(systemd_resolved):
words = line.lower().split() words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver': if len(words) >= 2 and words[0] == 'nameserver':
this_file_nsservers.append(family_ip_tuple(words[1])) this_file_nsservers.append(family_ip_tuple(words[1]))
debug2("Found DNS servers in %s: %s\n" % debug2("Found DNS servers in %s: %s" %
(f, [n[1] for n in this_file_nsservers])) (f, [n[1] for n in this_file_nsservers]))
nsservers += this_file_nsservers nsservers += this_file_nsservers
except OSError as e: except OSError as e:
debug3("Failed to read %s when looking for DNS servers: %s\n" % debug3("Failed to read %s when looking for DNS servers: %s" %
(f, e.strerror)) (f, e.strerror))
return nsservers return nsservers
@ -215,7 +223,7 @@ def which(file, mode=os.F_OK | os.X_OK):
path = get_path() path = get_path()
rv = _which(file, mode, path) rv = _which(file, mode, path)
if rv: if rv:
debug2("which() found '%s' at %s\n" % (file, rv)) debug2("which() found '%s' at %s" % (file, rv))
else: else:
debug2("which() could not find '%s' in %s\n" % (file, path)) debug2("which() could not find '%s' in %s" % (file, path))
return rv return rv

View File

@ -15,16 +15,16 @@ POLL_TIME = 60 * 15
NETSTAT_POLL_TIME = 30 NETSTAT_POLL_TIME = 30
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts') CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
# Have we already failed to write CACHEFILE?
CACHE_WRITE_FAILED = False
_nmb_ok = True
_smb_ok = True
hostnames = {} hostnames = {}
queue = {} queue = {}
try: try:
null = open(os.devnull, 'wb') null = open(os.devnull, 'wb')
except IOError: except IOError:
_, e = sys.exc_info()[:2] _, 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) null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
@ -33,7 +33,10 @@ def _is_ip(s):
def write_host_cache(): 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()) tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
global CACHE_WRITE_FAILED
try: try:
f = open(tmpname, 'wb') f = open(tmpname, 'wb')
for name, ip in sorted(hostnames.items()): for name, ip in sorted(hostnames.items()):
@ -41,7 +44,15 @@ def write_host_cache():
f.close() f.close()
os.chmod(tmpname, 384) # 600 in octal, 'rw-------' os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
os.rename(tmpname, CACHEFILE) 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: try:
os.unlink(tmpname) os.unlink(tmpname)
except BaseException: except BaseException:
@ -49,25 +60,34 @@ def write_host_cache():
def read_host_cache(): def read_host_cache():
"""If possible, read the cache file from disk to populate hosts that
were found in a previous sshuttle run."""
try: try:
f = open(CACHEFILE) f = open(CACHEFILE)
except IOError: except (OSError, IOError):
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
return return
else: else:
raise log("Failed to read existing host cache file %s on remote host"
% CACHEFILE)
return
for line in f: for line in f:
words = line.strip().split(',') words = line.strip().split(',')
if len(words) == 2: if len(words) == 2:
(name, ip) = words (name, ip) = words
name = re.sub(r'[^-\w\.]', '-', name).strip() name = re.sub(r'[^-\w\.]', '-', name).strip()
# Remove characters that shouldn't be in IP
ip = re.sub(r'[^0-9.]', '', ip).strip() ip = re.sub(r'[^0-9.]', '', ip).strip()
if name and ip: if name and ip:
found_host(name, ip) found_host(name, ip)
def 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'\..*', '', name)
hostname = re.sub(r'[^-\w\.]', '_', hostname) hostname = re.sub(r'[^-\w\.]', '_', hostname)
if (ip.startswith('127.') or ip.startswith('255.') or if (ip.startswith('127.') or ip.startswith('255.') or
@ -80,43 +100,51 @@ def found_host(name, ip):
oldip = hostnames.get(name) oldip = hostnames.get(name)
if oldip != ip: if oldip != ip:
hostnames[name] = 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)) sys.stdout.write('%s,%s\n' % (name, ip))
write_host_cache() write_host_cache()
def _check_etc_hosts(): def _check_etc_hosts():
debug2(' > hosts\n') """If possible, read /etc/hosts to find hosts."""
for line in open('/etc/hosts'): filename = '/etc/hosts'
line = re.sub(r'#.*', '', line) debug2(' > Reading %s on remote host' % filename)
words = line.strip().split() try:
if not words: for line in open(filename):
continue line = re.sub(r'#.*', '', line) # remove comments
ip = words[0] words = line.strip().split()
names = words[1:] if not words:
if _is_ip(ip): continue
debug3('< %s %r\n' % (ip, names)) ip = words[0]
for n in names: if _is_ip(ip):
check_host(n) names = words[1:]
found_host(n, ip) 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): 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: try:
r = socket.gethostbyaddr(ip) r = socket.gethostbyaddr(ip)
debug3('< %s\n' % r[0]) debug3('< %s' % r[0])
check_host(r[0]) check_host(r[0])
found_host(r[0], ip) found_host(r[0], ip)
except (socket.herror, UnicodeError): except (OSError, socket.error, UnicodeError):
# This case is expected to occur regularly.
# debug3('< %s gethostbyaddr failed on remote host' % ip)
pass pass
def _check_dns(hostname): def _check_dns(hostname):
debug2(' > dns: %s\n' % hostname) debug2(' > dns: %s' % hostname)
try: try:
ip = socket.gethostbyname(hostname) ip = socket.gethostbyname(hostname)
debug3('< %s\n' % ip) debug3('< %s' % ip)
check_host(ip) check_host(ip)
found_host(hostname, ip) found_host(hostname, ip)
except (socket.gaierror, UnicodeError): except (socket.gaierror, UnicodeError):
@ -124,7 +152,7 @@ def _check_dns(hostname):
def _check_netstat(): def _check_netstat():
debug2(' > netstat\n') debug2(' > netstat')
argv = ['netstat', '-n'] argv = ['netstat', '-n']
try: try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
@ -133,118 +161,26 @@ def _check_netstat():
p.wait() p.wait()
except OSError: except OSError:
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e)) log('%r failed: %r' % (argv, e))
return 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): 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) check_host(ip)
def _check_smb(hostname):
return
global _smb_ok
if not _smb_ok:
return
debug2(' > smb: %s\n' % hostname)
argv = ['smbclient', '-U', '%', '-L', hostname]
try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=get_env())
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
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
env=get_env)
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): def check_host(hostname):
if _is_ip(hostname): if _is_ip(hostname):
_enqueue(_check_revdns, hostname) _enqueue(_check_revdns, hostname)
else: else:
_enqueue(_check_dns, hostname) _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): def _enqueue(op, *args):
@ -263,12 +199,9 @@ def _stdin_still_ok(timeout):
def hw_main(seed_hosts, auto_hosts): def hw_main(seed_hosts, auto_hosts):
if helpers.verbose >= 2: helpers.logprefix = 'HH: '
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()) % platform.python_version())
for h in seed_hosts: for h in seed_hosts:
@ -280,18 +213,22 @@ def hw_main(seed_hosts, auto_hosts):
_enqueue(_check_netstat) _enqueue(_check_netstat)
check_host('localhost') check_host('localhost')
check_host(socket.gethostname()) check_host(socket.gethostname())
check_workgroup('workgroup')
check_workgroup('-')
while 1: while 1:
now = time.time() now = time.time()
# For each item in the queue
for t, last_polled in list(queue.items()): for t, last_polled in list(queue.items()):
(op, args) = t (op, args) = t
if not _stdin_still_ok(0): if not _stdin_still_ok(0):
break break
# Determine if we need to run.
maxtime = POLL_TIME maxtime = POLL_TIME
# netstat runs more often than other jobs
if op == _check_netstat: if op == _check_netstat:
maxtime = NETSTAT_POLL_TIME maxtime = NETSTAT_POLL_TIME
# Check if this jobs needs to run.
if now - last_polled > maxtime: if now - last_polled > maxtime:
queue[t] = time.time() queue[t] = time.time()
op(*args) op(*args)
@ -301,5 +238,5 @@ def hw_main(seed_hosts, auto_hosts):
break break
# FIXME: use a smarter timeout based on oldest last_polled # 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 break

View File

@ -7,7 +7,7 @@ def nonfatal(func, *args):
try: try:
func(*args) func(*args)
except Fatal as e: except Fatal as e:
log('fw: error: %s\n' % e) log('error: %s' % e)
def ipt_chain_exists(family, table, name): def ipt_chain_exists(family, table, name):
@ -17,27 +17,27 @@ def ipt_chain_exists(family, table, name):
cmd = 'iptables' cmd = 'iptables'
else: else:
raise Exception('Unsupported family "%s"' % family_to_string(family)) raise Exception('Unsupported family "%s"' % family_to_string(family))
argv = [cmd, '-t', table, '-nL'] argv = [cmd, '-w', '-t', table, '-nL']
try: try:
output = ssubprocess.check_output(argv, env=get_env()) output = ssubprocess.check_output(argv, env=get_env())
for line in output.decode('ASCII').split('\n'): for line in output.decode('ASCII').split('\n'):
if line.startswith('Chain %s ' % name): if line.startswith('Chain %s ' % name):
return True return True
except ssubprocess.CalledProcessError as e: except ssubprocess.CalledProcessError as e:
raise Fatal('fw: %r returned %d' % (argv, e.returncode)) raise Fatal('%r returned %d' % (argv, e.returncode))
def ipt(family, table, *args): def ipt(family, table, *args):
if family == socket.AF_INET6: if family == socket.AF_INET6:
argv = ['ip6tables', '-t', table] + list(args) argv = ['ip6tables', '-w', '-t', table] + list(args)
elif family == socket.AF_INET: elif family == socket.AF_INET:
argv = ['iptables', '-t', table] + list(args) argv = ['iptables', '-w', '-t', table] + list(args)
else: else:
raise Exception('Unsupported family "%s"' % family_to_string(family)) raise Exception('Unsupported family "%s"' % family_to_string(family))
debug1('%s\n' % ' '.join(argv)) debug1('%s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env()) rv = ssubprocess.call(argv, env=get_env())
if rv: if rv:
raise Fatal('fw: %r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
def nft(family, table, action, *args): def nft(family, table, action, *args):
@ -45,29 +45,7 @@ def nft(family, table, action, *args):
argv = ['nft', action, 'inet', table] + list(args) argv = ['nft', action, 'inet', table] + list(args)
else: else:
raise Exception('Unsupported family "%s"' % family_to_string(family)) raise Exception('Unsupported family "%s"' % family_to_string(family))
debug1('%s\n' % ' '.join(argv)) debug1('%s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env()) rv = ssubprocess.call(argv, env=get_env())
if rv: if rv:
raise Fatal('fw: %r returned %d' % (argv, 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 63. This makes the client side not recapture those
# connections, in case client == server.
try:
argsplus = list(args)
ipt(family, *argsplus)
except Fatal:
ipt(family, *args)
# we only get here if the non-ttl attempt succeeds
log('fw: WARNING: your iptables is missing '
'the ttl module.\n')
_no_ttl_module = True
else:
ipt(family, *args)

View File

@ -66,7 +66,7 @@ class BaseMethod(object):
@staticmethod @staticmethod
def recv_udp(udp_listener, bufsize): def recv_udp(udp_listener, bufsize):
debug3('Accept UDP using recvfrom.\n') debug3('Accept UDP using recvfrom.')
data, srcip = udp_listener.recvfrom(bufsize) data, srcip = udp_listener.recvfrom(bufsize)
return (srcip, None, data) return (srcip, None, data)
@ -87,11 +87,11 @@ class BaseMethod(object):
for key in ["udp", "dns", "ipv6", "ipv4", "user"]: for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
if getattr(features, key) and not getattr(avail, key): if getattr(features, key) and not getattr(avail, key):
raise Fatal( raise Fatal(
"Feature %s not supported with method %s.\n" % "Feature %s not supported with method %s." %
(key, self.name)) (key, self.name))
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user): user, tmark):
raise NotImplementedError() raise NotImplementedError()
def restore_firewall(self, port, family, udp, user): def restore_firewall(self, port, family, udp, user):
@ -108,13 +108,13 @@ def get_method(method_name):
def get_auto_method(): def get_auto_method():
debug3("Selecting a method automatically...\n") debug3("Selecting a method automatically...")
# Try these methods, in order: # Try these methods, in order:
methods_to_try = ["nat", "nft", "pf", "ipfw"] methods_to_try = ["nat", "nft", "pf", "ipfw"]
for m in methods_to_try: for m in methods_to_try:
method = get_method(m) method = get_method(m)
if method.is_supported(): if method.is_supported():
debug3("Method '%s' was automatically selected.\n" % m) debug3("Method '%s' was automatically selected." % m)
return method return method
raise Fatal("Unable to automatically find a supported method. Check that " raise Fatal("Unable to automatically find a supported method. Check that "

View File

@ -4,72 +4,40 @@ from sshuttle.methods import BaseMethod
from sshuttle.helpers import log, debug1, debug2, debug3, \ from sshuttle.helpers import log, debug1, debug2, debug3, \
Fatal, family_to_string, get_env, which Fatal, family_to_string, get_env, which
recvmsg = None import socket
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
recvmsg = "python"
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
recvmsg = "socket_ext"
except ImportError:
import socket
IP_BINDANY = 24 IP_BINDANY = 24
IP_RECVDSTADDR = 7 IP_RECVDSTADDR = 7
SOL_IPV6 = 41 SOL_IPV6 = 41
IPV6_RECVDSTADDR = 74 IPV6_RECVDSTADDR = 74
if recvmsg == "python":
def recv_udp(listener, bufsize): def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n') debug3('Accept UDP python using recvmsg.')
data, ancdata, _, srcip = listener.recvmsg(4096, data, ancdata, _, srcip = listener.recvmsg(4096,
socket.CMSG_SPACE(4)) socket.CMSG_SPACE(4))
dstip = None dstip = None
for cmsg_level, cmsg_type, cmsg_data in ancdata: for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR: if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
port = 53 port = 53
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4]) ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
dstip = (ip, port) dstip = (ip, port)
break break
return (srcip, dstip, data) 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): def ipfw_rule_exists(n):
argv = ['ipfw', 'list'] argv = ['ipfw', 'list', '%d' % n]
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env()) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
found = False found = False
for line in p.stdout: for line in p.stdout:
if line.startswith(b'%05d ' % n): if line.startswith(b'%05d ' % n):
if not ('ipttl 63' in line or 'check-state' in line): if 'check-state :sshuttle' not in line:
log('non-sshuttle ipfw rule: %r\n' % line.strip()) log('non-sshuttle ipfw rule: %r' % line.strip())
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n) raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
found = True found = True
break
rv = p.wait() rv = p.wait()
if rv: if rv:
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
@ -96,7 +64,7 @@ def _fill_oldctls(prefix):
def _sysctl_set(name, val): def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)] argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s' % ' '.join(argv))
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env()) return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
# No env: No output. (Or error that won't be parsed.) # No env: No output. (Or error that won't be parsed.)
@ -111,13 +79,13 @@ def sysctl_set(name, val, permanent=False):
if not _oldctls: if not _oldctls:
_fill_oldctls(PREFIX) _fill_oldctls(PREFIX)
if not (name in _oldctls): if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name) debug1('>> No such sysctl: %r' % name)
return False return False
oldval = _oldctls[name] oldval = _oldctls[name]
if val != oldval: if val != oldval:
rv = _sysctl_set(name, val) rv = _sysctl_set(name, val)
if rv == 0 and permanent: 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 = open('/etc/sysctl.conf', 'a')
f.write('\n' f.write('\n'
'# Added by sshuttle\n' '# Added by sshuttle\n'
@ -130,7 +98,7 @@ def sysctl_set(name, val, permanent=False):
def ipfw(*args): def ipfw(*args):
argv = ['ipfw', '-q'] + list(args) argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env()) rv = ssubprocess.call(argv, env=get_env())
# No env: No output. (Or error that won't be parsed.) # No env: No output. (Or error that won't be parsed.)
if rv: if rv:
@ -139,7 +107,7 @@ def ipfw(*args):
def ipfw_noexit(*args): def ipfw_noexit(*args):
argv = ['ipfw', '-q'] + list(args) argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s' % ' '.join(argv))
ssubprocess.call(argv, env=get_env()) ssubprocess.call(argv, env=get_env())
# No env: No output. (Or error that won't be parsed.) # No env: No output. (Or error that won't be parsed.)
@ -161,7 +129,7 @@ class Method(BaseMethod):
if not dstip: if not dstip:
debug1( debug1(
"-- ignored UDP from %r: " "-- ignored UDP from %r: "
"couldn't determine destination IP address\n" % (srcip,)) "couldn't determine destination IP address" % (srcip,))
return None return None
return srcip, dstip, data return srcip, dstip, data
@ -169,15 +137,14 @@ class Method(BaseMethod):
if not srcip: if not srcip:
debug1( debug1(
"-- ignored UDP to %r: " "-- ignored UDP to %r: "
"couldn't determine source IP address\n" % (dstip,)) "couldn't determine source IP address" % (dstip,))
return 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 = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1) sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
sender.bind(srcip) sender.bind(srcip)
sender.sendto(data, dstip) sender.sendto(data, dstip)
sender.close() sender.close()
@ -189,7 +156,7 @@ class Method(BaseMethod):
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1) # udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user): user, tmark):
# IPv6 not supported # IPv6 not supported
if family not in [socket.AF_INET]: if family not in [socket.AF_INET]:
raise Exception( raise Exception(
@ -207,8 +174,7 @@ class Method(BaseMethod):
if subnets or dnsport: if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1) sysctl_set('net.inet.ip.fw.enable', 1)
ipfw('add', '1', 'check-state', 'ip', ipfw('add', '1', 'check-state', ':sshuttle')
'from', 'any', 'to', 'any')
ipfw('add', '1', 'skipto', '2', ipfw('add', '1', 'skipto', '2',
'tcp', 'tcp',
@ -216,7 +182,7 @@ class Method(BaseMethod):
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port, ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
'tcp', 'tcp',
'from', 'any', 'to', 'table(126)', 'from', 'any', 'to', 'table(126)',
'not', 'ipttl', '63', 'keep-state', 'setup') 'setup', 'keep-state', ':sshuttle')
ipfw_noexit('table', '124', 'flush') ipfw_noexit('table', '124', 'flush')
dnscount = 0 dnscount = 0
@ -227,26 +193,24 @@ class Method(BaseMethod):
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport, ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
'udp', 'udp',
'from', 'any', 'to', 'table(124)', 'from', 'any', 'to', 'table(124)',
'not', 'ipttl', '63') 'keep-state', ':sshuttle')
ipfw('add', '1', 'allow', ipfw('add', '1', 'allow',
'udp', 'udp',
'from', 'any', 'to', 'any', 'from', 'any', 'to', 'any')
'ipttl', '63')
if subnets: if subnets:
# create new subnet entries # create new subnet entries
for _, swidth, sexclude, snet in sorted(subnets, for _, swidth, sexclude, snet, fport, lport \
key=lambda s: s[1], in sorted(subnets, key=lambda s: s[1], reverse=True):
reverse=True):
if sexclude: if sexclude:
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth)) ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
else: else:
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth)) ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
def restore_firewall(self, port, family, udp, user): def restore_firewall(self, port, family, udp, user):
if family not in [socket.AF_INET]: if family not in [socket.AF_INET]:
raise Exception( raise Exception(
'Address family "%s" unsupported by tproxy method' 'Address family "%s" unsupported by ipfw method'
% family_to_string(family)) % family_to_string(family))
ipfw_noexit('delete', '1') ipfw_noexit('delete', '1')
@ -258,5 +222,5 @@ class Method(BaseMethod):
if which("ipfw"): if which("ipfw"):
return True return True
debug2("ipfw method not supported because 'ipfw' command is " debug2("ipfw method not supported because 'ipfw' command is "
"missing.\n") "missing.")
return False return False

View File

@ -1,7 +1,7 @@
import socket import socket
from sshuttle.firewall import subnet_weight from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string, which, debug2 from sshuttle.helpers import family_to_string, which, debug2
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal from sshuttle.linux import ipt, ipt_chain_exists, nonfatal
from sshuttle.methods import BaseMethod 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 # recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT"). # "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user): user, tmark):
# only ipv4 supported with NAT if family != socket.AF_INET and family != socket.AF_INET6:
if family != socket.AF_INET:
raise Exception( raise Exception(
'Address family "%s" unsupported by nat method_name' 'Address family "%s" unsupported by nat method_name'
% family_to_string(family)) % family_to_string(family))
if udp: if udp:
raise Exception("UDP not supported by nat method_name") raise Exception("UDP not supported by nat method_name")
table = "nat" table = "nat"
def _ipt(*args): def _ipt(*args):
return ipt(family, table, *args) return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipm(*args): def _ipm(*args):
return ipt(family, "mangle", *args) return ipt(family, "mangle", *args)
@ -50,16 +45,11 @@ class Method(BaseMethod):
_ipt('-I', 'OUTPUT', '1', *args) _ipt('-I', 'OUTPUT', '1', *args)
_ipt('-I', 'PREROUTING', '1', *args) _ipt('-I', 'PREROUTING', '1', *args)
# This TTL hack allows the client and server to run on the
# same host. The connections the sshuttle server makes will
# have TTL set to 63.
_ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', '63')
# Redirect DNS traffic as requested. This includes routing traffic # Redirect DNS traffic as requested. This includes routing traffic
# to localhost DNS servers through sshuttle. # to localhost DNS servers through sshuttle.
for _, ip in [i for i in nslist if i[0] == family]: for _, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', chain, '-j', 'REDIRECT', _ipt('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip, '--dest', '%s' % ip,
'-p', 'udp', '-p', 'udp',
'--dport', '53', '--dport', '53',
'--to-ports', str(dnsport)) '--to-ports', str(dnsport))
@ -87,7 +77,7 @@ class Method(BaseMethod):
def restore_firewall(self, port, family, udp, user): def restore_firewall(self, port, family, udp, user):
# only ipv4 supported with NAT # only ipv4 supported with NAT
if family != socket.AF_INET: if family != socket.AF_INET and family != socket.AF_INET6:
raise Exception( raise Exception(
'Address family "%s" unsupported by nat method_name' 'Address family "%s" unsupported by nat method_name'
% family_to_string(family)) % family_to_string(family))
@ -99,9 +89,6 @@ class Method(BaseMethod):
def _ipt(*args): def _ipt(*args):
return ipt(family, table, *args) return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipm(*args): def _ipm(*args):
return ipt(family, "mangle", *args) return ipt(family, "mangle", *args)
@ -123,11 +110,12 @@ class Method(BaseMethod):
def get_supported_features(self): def get_supported_features(self):
result = super(Method, self).get_supported_features() result = super(Method, self).get_supported_features()
result.user = True result.user = True
result.ipv6 = True
return result return result
def is_supported(self): def is_supported(self):
if which("iptables"): if which("iptables"):
return True return True
debug2("nat method not supported because 'iptables' command " debug2("nat method not supported because 'iptables' command "
"is missing.\n") "is missing.")
return False return False

View File

@ -13,7 +13,7 @@ class Method(BaseMethod):
# recently-started one will win (because we use "-I OUTPUT 1" instead of # recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT"). # "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user): user, tmark):
if udp: if udp:
raise Exception("UDP not supported by nft") raise Exception("UDP not supported by nft")
@ -45,14 +45,6 @@ class Method(BaseMethod):
else: else:
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return') _nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return')
# This TTL hack allows the client and server to run on the
# same host. The connections the sshuttle server makes will
# have TTL set to 63.
if family == socket.AF_INET:
_nft('add rule', chain, 'ip ttl == 63 return')
elif family == socket.AF_INET6:
_nft('add rule', chain, 'ip6 hoplimit == 63 return')
# Strings to use below to simplify our code # Strings to use below to simplify our code
if family == socket.AF_INET: if family == socket.AF_INET:
ip_version_l = 'ipv4' ip_version_l = 'ipv4'
@ -118,5 +110,5 @@ class Method(BaseMethod):
def is_supported(self): def is_supported(self):
if which("nft"): if which("nft"):
return True return True
debug2("nft method not supported because 'nft' command is missing.\n") debug2("nft method not supported because 'nft' command is missing.")
return False return False

View File

@ -11,8 +11,8 @@ from fcntl import ioctl
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \ from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
sizeof, addressof, memmove sizeof, addressof, memmove
from sshuttle.firewall import subnet_weight 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, \
get_env, which family_to_string, get_env, which
from sshuttle.methods import BaseMethod from sshuttle.methods import BaseMethod
@ -273,7 +273,7 @@ class OpenBsd(Generic):
def add_anchors(self, anchor): def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo # before adding anchors and rules we must override the skip lo
# that comes by default in openbsd pf.conf so the rules we will add, # that comes by default in openbsd pf.conf so the rules we will add,
# which rely on translating/filtering packets on lo, can work # which rely on translating/filtering packets on lo, can work
if self.has_skip_loopback(): if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'match on lo\n') pfctl('-f /dev/stdin', b'match on lo\n')
super(OpenBsd, self).add_anchors(anchor) super(OpenBsd, self).add_anchors(anchor)
@ -353,7 +353,7 @@ class Darwin(FreeBsd):
def add_anchors(self, anchor): def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo # before adding anchors and rules we must override the skip lo
# that in some cases ends up in the chain so the rules we will add, # that in some cases ends up in the chain so the rules we will add,
# which rely on translating/filtering packets on lo, can work # which rely on translating/filtering packets on lo, can work
if self.has_skip_loopback(): if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'pass on lo\n') pfctl('-f /dev/stdin', b'pass on lo\n')
super(Darwin, self).add_anchors(anchor) super(Darwin, self).add_anchors(anchor)
@ -386,13 +386,17 @@ else:
def pfctl(args, stdin=None): def pfctl(args, stdin=None):
argv = ['pfctl'] + shlex.split(args) argv = ['pfctl'] + shlex.split(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s' % ' '.join(argv))
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE, p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
stdout=ssubprocess.PIPE, stdout=ssubprocess.PIPE,
stderr=ssubprocess.PIPE, stderr=ssubprocess.PIPE,
env=get_env()) env=get_env())
o = p.communicate(stdin) o = p.communicate(stdin)
if p.returncode: 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)) raise Fatal('%r returned %d' % (argv, p.returncode))
return o return o
@ -444,7 +448,7 @@ class Method(BaseMethod):
return sock.getsockname() return sock.getsockname()
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user): user, tmark):
if family not in [socket.AF_INET, socket.AF_INET6]: if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception( raise Exception(
'Address family "%s" unsupported by pf method_name' 'Address family "%s" unsupported by pf method_name'
@ -495,5 +499,5 @@ class Method(BaseMethod):
def is_supported(self): def is_supported(self):
if which("pfctl"): if which("pfctl"):
return True return True
debug2("pf method not supported because 'pfctl' command is missing.\n") debug2("pf method not supported because 'pfctl' command is missing.")
return False return False

View File

@ -1,26 +1,12 @@
import struct import struct
from sshuttle.firewall import subnet_weight from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string 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.methods import BaseMethod
from sshuttle.helpers import debug1, debug2, debug3, Fatal, which from sshuttle.helpers import debug1, debug2, debug3, Fatal, which
recvmsg = None import socket
try: import os
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
recvmsg = "python"
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
recvmsg = "socket_ext"
except ImportError:
import socket
IP_TRANSPARENT = 19 IP_TRANSPARENT = 19
@ -30,75 +16,37 @@ SOL_IPV6 = 41
IPV6_ORIGDSTADDR = 74 IPV6_ORIGDSTADDR = 74
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
if recvmsg == "python":
def recv_udp(listener, bufsize): def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n') debug3('Accept UDP python using recvmsg.')
data, ancdata, _, srcip = listener.recvmsg( data, ancdata, _, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24)) 4096, socket.CMSG_SPACE(24))
dstip = None dstip = None
family = None family = None
for cmsg_level, cmsg_type, cmsg_data in ancdata: for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR: if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4]) family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port) port = socket.htons(port)
if family == socket.AF_INET: if family == socket.AF_INET:
start = 4 start = 4
length = 4 length = 4
else: else:
raise Fatal("Unsupported socket type '%s'" % family) raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length]) ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port) dstip = (ip, port)
break break
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR: elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4]) family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port) port = socket.htons(port)
if family == socket.AF_INET6: if family == socket.AF_INET6:
start = 8 start = 8
length = 16 length = 16
else: else:
raise Fatal("Unsupported socket type '%s'" % family) raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length]) ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port) dstip = (ip, port)
break break
return (srcip, dstip, data) 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): class Method(BaseMethod):
@ -106,12 +54,8 @@ class Method(BaseMethod):
def get_supported_features(self): def get_supported_features(self):
result = super(Method, self).get_supported_features() result = super(Method, self).get_supported_features()
result.ipv6 = True result.ipv6 = True
if recvmsg is None: result.udp = True
result.udp = False result.dns = True
result.dns = False
else:
result.udp = True
result.dns = True
return result return result
def get_tcp_dstip(self, sock): def get_tcp_dstip(self, sock):
@ -126,6 +70,15 @@ class Method(BaseMethod):
return None return None
return srcip, dstip, data 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): def send_udp(self, sock, srcip, dstip, data):
if not srcip: if not srcip:
debug1( debug1(
@ -134,16 +87,26 @@ class Method(BaseMethod):
return return
sender = socket.socket(sock.family, socket.SOCK_DGRAM) sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) try:
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
except PermissionError as e:
self.setsockopt_error(e)
sender.bind(srcip) sender.bind(srcip)
sender.sendto(data, dstip) sender.sendto(data, dstip)
sender.close() sender.close()
def setup_tcp_listener(self, tcp_listener): def setup_tcp_listener(self, tcp_listener):
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 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): def setup_udp_listener(self, udp_listener):
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 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: if udp_listener.v4 is not None:
udp_listener.v4.setsockopt( udp_listener.v4.setsockopt(
socket.SOL_IP, IP_RECVORIGDSTADDR, 1) socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
@ -151,17 +114,7 @@ class Method(BaseMethod):
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user): user, tmark):
if self.firewall is None:
tmark = '1'
else:
tmark = self.firewall.tmark
self.setup_firewall_tproxy(port, dnsport, nslist, family, subnets, udp,
user, tmark)
def setup_firewall_tproxy(self, port, dnsport, nslist, family, subnets,
udp, user, tmark):
if family not in [socket.AF_INET, socket.AF_INET6]: if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception( raise Exception(
'Address family "%s" unsupported by tproxy method' 'Address family "%s" unsupported by tproxy method'
@ -172,9 +125,6 @@ class Method(BaseMethod):
def _ipt(*args): def _ipt(*args):
return ipt(family, table, *args) return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipt_proto_ports(proto, fport, lport): def _ipt_proto_ports(proto, fport, lport):
return proto + ('--dport', '%d:%d' % (fport, lport)) \ return proto + ('--dport', '%d:%d' % (fport, lport)) \
if fport else proto if fport else proto
@ -192,8 +142,24 @@ class Method(BaseMethod):
_ipt('-F', divert_chain) _ipt('-F', divert_chain)
_ipt('-N', tproxy_chain) _ipt('-N', tproxy_chain)
_ipt('-F', tproxy_chain) _ipt('-F', tproxy_chain)
_ipt('-I', 'OUTPUT', tmark, '-j', mark_chain) _ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
_ipt('-I', 'PREROUTING', tmark, '-j', tproxy_chain) _ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
# 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', 'MARK', '--set-mark', tmark)
_ipt('-A', divert_chain, '-j', 'ACCEPT') _ipt('-A', divert_chain, '-j', 'ACCEPT')
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, _ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
@ -208,7 +174,7 @@ class Method(BaseMethod):
'--dest', '%s/32' % ip, '--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53') '-m', 'udp', '-p', 'udp', '--dport', '53')
_ipt('-A', tproxy_chain, '-j', 'TPROXY', _ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x'+tmark+'/0x'+tmark, '--tproxy-mark', tmark,
'--dest', '%s/32' % ip, '--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53', '-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport)) '--on-port', str(dnsport))
@ -233,7 +199,7 @@ class Method(BaseMethod):
'-m', 'tcp', '-m', 'tcp',
*tcp_ports) *tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY', _ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x'+tmark+'/0x'+tmark, '--tproxy-mark', tmark,
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-m', 'tcp',
*(tcp_ports + ('--on-port', str(port)))) *(tcp_ports + ('--on-port', str(port))))
@ -257,7 +223,7 @@ class Method(BaseMethod):
'-m', 'udp', '-m', 'udp',
*udp_ports) *udp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY', _ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x'+tmark+'/0x'+tmark, '--tproxy-mark', tmark,
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-m', 'udp',
*(udp_ports + ('--on-port', str(port)))) *(udp_ports + ('--on-port', str(port))))
@ -273,9 +239,6 @@ class Method(BaseMethod):
def _ipt(*args): def _ipt(*args):
return ipt(family, table, *args) return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
mark_chain = 'sshuttle-m-%s' % port mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port divert_chain = 'sshuttle-d-%s' % port

View File

@ -132,6 +132,7 @@ def parse_ipport(s):
def parse_list(lst): def parse_list(lst):
"""Parse a comma separated string into a list."""
return re.split(r'[\s,]+', lst.strip()) if lst else [] return re.split(r'[\s,]+', lst.strip()) if lst else []
@ -146,9 +147,33 @@ class Concat(Action):
setattr(namespace, self.dest, curr_value + values) 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", 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="@" fromfile_prefix_chars="@"
) )
parser.add_argument( parser.add_argument(
@ -196,6 +221,7 @@ parser.add_argument(
type=parse_list, type=parse_list,
help=""" help="""
capture and forward DNS requests made to the following servers capture and forward DNS requests made to the following servers
(comma separated)
""" """
) )
parser.add_argument( parser.add_argument(
@ -256,7 +282,7 @@ parser.add_argument(
action="count", action="count",
default=0, default=0,
help=""" help="""
increase debug message verbosity increase debug message verbosity (can be used more than once)
""" """
) )
parser.add_argument( parser.add_argument(
@ -412,8 +438,9 @@ parser.add_argument(
parser.add_argument( parser.add_argument(
"-t", "--tmark", "-t", "--tmark",
metavar="[MARK]", metavar="[MARK]",
default="1", default="0x01",
help=""" help="""
transproxy optional traffic mark with provided MARK value tproxy optional traffic mark with provided MARK value in
hexadecimal (default '0x01')
""" """
) )

View File

@ -26,7 +26,7 @@ def _notify(message):
try: try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
except (OSError, IOError) as e: 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 return False
if not message: if not message:
@ -37,7 +37,7 @@ def _notify(message):
try: try:
return (sock.sendto(message, addr) > 0) return (sock.sendto(message, addr) > 0)
except (OSError, IOError) as e: except (OSError, IOError) as e:
debug1("Error notifying systemd: %s\n" % e) debug1("Error notifying systemd: %s" % e)
return False return False

View File

@ -5,7 +5,6 @@ import traceback
import time import time
import sys import sys
import os import os
import platform
import sshuttle.ssnet as ssnet import sshuttle.ssnet as ssnet
@ -96,7 +95,7 @@ def _list_routes(argv, extract_route):
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width)) (socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait() rv = p.wait()
if rv != 0: if rv != 0:
log('WARNING: %r returned %d\n' % (argv, rv)) log('WARNING: %r returned %d' % (argv, rv))
return routes return routes
@ -108,7 +107,7 @@ def list_routes():
routes = _list_routes(['netstat', '-rn'], _route_netstat) routes = _list_routes(['netstat', '-rn'], _route_netstat)
else: else:
log('WARNING: Neither "ip" nor "netstat" were found on the server. ' log('WARNING: Neither "ip" nor "netstat" were found on the server. '
'--auto-nets feature will not work.\n') '--auto-nets feature will not work.')
routes = [] routes = []
for (family, ip, width) in routes: for (family, ip, width) in routes:
@ -135,7 +134,7 @@ def start_hostwatch(seed_hosts, auto_hosts):
s1.close() s1.close()
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0 rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
except Exception: except Exception:
log('%s\n' % _exc_dump()) log('%s' % _exc_dump())
rv = 98 rv = 98
finally: finally:
os._exit(rv) os._exit(rv)
@ -191,12 +190,11 @@ class DnsProxy(Handler):
family, sockaddr = self._addrinfo(peer, port) family, sockaddr = self._addrinfo(peer, port)
sock = socket.socket(family, socket.SOCK_DGRAM) sock = socket.socket(family, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
sock.connect(sockaddr) sock.connect(sockaddr)
self.peers[sock] = peer 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: try:
sock.send(self.request) sock.send(self.request)
self.socks.append(sock) self.socks.append(sock)
@ -206,11 +204,11 @@ class DnsProxy(Handler):
# might have been spurious; try again. # might have been spurious; try again.
# Note: these errors sometimes are reported by recv(), # Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both. # 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() self.try_send()
return return
else: else:
log('DNS send to %r: %s\n' % (peer, e)) log('DNS send to %r: %s' % (peer, e))
return return
def callback(self, sock): def callback(self, sock):
@ -227,13 +225,13 @@ class DnsProxy(Handler):
# might have been spurious; try again. # might have been spurious; try again.
# Note: these errors sometimes are reported by recv(), # Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both. # 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() self.try_send()
return return
else: else:
log('DNS recv from %r: %s\n' % (peer, e)) log('DNS recv from %r: %s' % (peer, e))
return 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.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
self.ok = False self.ok = False
@ -247,16 +245,14 @@ class UdpProxy(Handler):
self.mux = mux self.mux = mux
self.chan = chan self.chan = chan
self.sock = sock self.sock = sock
if family == socket.AF_INET:
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
def send(self, dstip, data): def send(self, dstip, data):
debug2(' s: UDP: sending to %r port %d\n' % dstip) debug2('UDP: sending to %r port %d' % dstip)
try: try:
self.sock.sendto(data, dstip) self.sock.sendto(data, dstip)
except socket.error: except socket.error:
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
log(' s: 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 return
def callback(self, sock): def callback(self, sock):
@ -264,147 +260,157 @@ class UdpProxy(Handler):
data, peer = sock.recvfrom(4096) data, peer = sock.recvfrom(4096)
except socket.error: except socket.error:
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
log(' s: 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 return
debug2(' s: UDP response: %d bytes\n' % len(data)) debug2('UDP response: %d bytes' % len(data))
hdr = b("%s,%r," % (peer[0], peer[1])) hdr = b("%s,%r," % (peer[0], peer[1]))
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data) self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
def main(latency_control, auto_hosts, to_nameserver, auto_nets): def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
debug1(' s: Starting server with Python version %s\n' auto_nets):
% platform.python_version()) try:
helpers.logprefix = ' s: '
helpers.logprefix = ' s: ' debug1('latency control setting = %r' % latency_control)
debug1('latency control setting = %r\n' % latency_control) if latency_buffer_size:
import sshuttle.ssnet as ssnet
ssnet.LATENCY_BUFFER_SIZE = latency_buffer_size
# synchronization header # synchronization header
sys.stdout.write('\0\0SSHUTTLE0001') sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush() sys.stdout.flush()
handlers = [] handlers = []
mux = Mux(sys.stdin, sys.stdout) mux = Mux(sys.stdin, sys.stdout)
handlers.append(mux) handlers.append(mux)
debug1('auto-nets:' + str(auto_nets) + '\n') debug1('auto-nets:' + str(auto_nets))
if auto_nets: if auto_nets:
routes = list(list_routes()) routes = list(list_routes())
debug1('available routes:\n') debug1('available routes:')
for r in routes:
debug1(' %d/%s/%d' % r)
else:
routes = []
routepkt = ''
for r in routes: for r in routes:
debug1(' %d/%s/%d\n' % r) routepkt += '%d,%s,%d\n' % r
else: mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
routes = []
routepkt = '' hw = Hostwatch()
for r in routes: hw.leftover = b('')
routepkt += '%d,%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
hw = Hostwatch() def hostwatch_ready(sock):
hw.leftover = b('') assert(hw.pid)
content = hw.sock.recv(4096)
def hostwatch_ready(sock): if content:
assert(hw.pid) lines = (hw.leftover + content).split(b('\n'))
content = hw.sock.recv(4096) if lines[-1]:
if content: # no terminating newline: entry isn't complete yet!
lines = (hw.leftover + content).split(b('\n')) hw.leftover = lines.pop()
if lines[-1]: lines.append(b(''))
# no terminating newline: entry isn't complete yet! else:
hw.leftover = lines.pop() hw.leftover = b('')
lines.append(b('')) mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
else: else:
hw.leftover = b('') raise Fatal('hostwatch process died')
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
else:
raise Fatal(' s: hostwatch process died')
def got_host_req(data): def got_host_req(data):
if not hw.pid: if not hw.pid:
(hw.pid, hw.sock) = start_hostwatch( (hw.pid, hw.sock) = start_hostwatch(
data.decode("ASCII").strip().split(), auto_hosts) data.decode("ASCII").strip().split(), auto_hosts)
handlers.append(Handler(socks=[hw.sock], handlers.append(Handler(socks=[hw.sock],
callback=hostwatch_ready)) callback=hostwatch_ready))
mux.got_host_req = got_host_req mux.got_host_req = got_host_req
def new_channel(channel, data): def new_channel(channel, data):
(family, dstip, dstport) = data.decode("ASCII").split(',', 2) (family, dstip, dstport) = data.decode("ASCII").split(',', 2)
family = int(family) family = int(family)
# AF_INET is the same constant on Linux and BSD but AF_INET6 # AF_INET is the same constant on Linux and BSD but AF_INET6
# is different. As the client and server can be running on # is different. As the client and server can be running on
# different platforms we can not just set the socket family # different platforms we can not just set the socket family
# to what comes in the wire. # to what comes in the wire.
if family != socket.AF_INET: if family != socket.AF_INET:
family = socket.AF_INET6 family = socket.AF_INET6
dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
dnshandlers = {}
def dns_req(channel, data):
debug2('Incoming DNS request channel=%d.\n' % channel)
h = DnsProxy(mux, channel, data, to_nameserver)
handlers.append(h)
dnshandlers[channel] = h
mux.got_dns_req = dns_req
udphandlers = {}
def udp_req(channel, cmd, data):
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
if cmd == ssnet.CMD_UDP_DATA:
(dstip, dstport, data) = data.split(b(','), 2)
dstport = int(dstport) dstport = int(dstport)
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport)) outwrap = ssnet.connect_dst(family, dstip, dstport)
h = udphandlers[channel] handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
h.send((dstip, dstport), data) mux.new_channel = new_channel
elif cmd == ssnet.CMD_UDP_CLOSE:
debug2('is incoming UDP close\n')
h = udphandlers[channel]
h.ok = False
del mux.channels[channel]
def udp_open(channel, data): dnshandlers = {}
debug2('Incoming UDP open.\n')
family = int(data) def dns_req(channel, data):
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data) debug2('Incoming DNS request channel=%d.' % channel)
if channel in udphandlers: h = DnsProxy(mux, channel, data, to_nameserver)
raise Fatal(' s: UDP connection channel %d already open' % channel)
else:
h = UdpProxy(mux, channel, family)
handlers.append(h) handlers.append(h)
udphandlers[channel] = h dnshandlers[channel] = h
mux.got_udp_open = udp_open mux.got_dns_req = dns_req
while mux.ok: udphandlers = {}
if hw.pid:
assert(hw.pid > 0)
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
ssnet.runonce(handlers, mux) def udp_req(channel, cmd, data):
if latency_control: debug2('Incoming UDP request channel=%d, cmd=%d' %
mux.check_fullness() (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.' % (dstip, dstport))
h = udphandlers[channel]
h.send((dstip, dstport), data)
elif cmd == ssnet.CMD_UDP_CLOSE:
debug2('is incoming UDP close')
h = udphandlers[channel]
h.ok = False
del mux.channels[channel]
if dnshandlers: def udp_open(channel, data):
now = time.time() debug2('Incoming UDP open.')
remove = [] family = int(data)
for channel, h in dnshandlers.items(): mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd,
if h.timeout < now or not h.ok: data)
debug3('expiring dnsreqs channel=%d\n' % channel) if channel in udphandlers:
remove.append(channel) raise Fatal('UDP connection channel %d already open' %
h.ok = False channel)
for channel in remove: else:
del dnshandlers[channel] h = UdpProxy(mux, channel, family)
if udphandlers: handlers.append(h)
remove = [] udphandlers[channel] = h
for channel, h in udphandlers.items(): mux.got_udp_open = udp_open
if not h.ok:
debug3('expiring UDP channel=%d\n' % channel) while mux.ok:
remove.append(channel) if hw.pid:
h.ok = False assert(hw.pid > 0)
for channel in remove: (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
del udphandlers[channel] if rpid:
raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
if dnshandlers:
now = time.time()
remove = []
for channel, h in dnshandlers.items():
if h.timeout < now or not h.ok:
debug3('expiring dnsreqs channel=%d' % channel)
remove.append(channel)
h.ok = False
for channel in remove:
del dnshandlers[channel]
if udphandlers:
remove = []
for channel, h in udphandlers.items():
if not h.ok:
debug3('expiring UDP channel=%d' % 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

@ -61,11 +61,11 @@ def parse_hostport(rhostport):
if ":" in host: if ":" in host:
# IPv6 address and/or got a port specified # IPv6 address and/or got a port specified
# If it is an IPv6 adress with port specification, # If it is an IPv6 address with port specification,
# then it will look like: [::1]:22 # then it will look like: [::1]:22
try: try:
# try to parse host as an IP adress, # try to parse host as an IP address,
# if that works it is an IPv6 address # if that works it is an IPv6 address
host = str(ipaddress.ip_address(host)) host = str(ipaddress.ip_address(host))
except ValueError: except ValueError:
@ -103,11 +103,21 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
empackage(z, 'sshuttle.server') + empackage(z, 'sshuttle.server') +
b"\n") 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""" pyscript = r"""
import sys, os; import sys, os;
verbosity=%d; verbosity=%d;
sys.stdin = os.fdopen(0, "rb"); 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)) """ % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip()) pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@ -127,8 +137,47 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
if python: if python:
pycmd = "'%s' -c '%s'" % (python, pyscript) pycmd = "'%s' -c '%s'" % (python, pyscript)
else: else:
# 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; " pycmd = ("P=python3; $P -V 2>%s || P=python; "
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript)) "exec \"$P\" -c %s; exit 97") % \
(os.devnull, quote(pyscript))
pycmd = ("/bin/sh -c {}".format(quote(pycmd))) pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
if password is not None: if password is not None:
@ -160,7 +209,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close() s1.close()
debug2('executing: %r\n' % argv) debug2('executing: %r' % argv)
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True, stderr=stderr) close_fds=True, stderr=stderr)
os.close(s1a) os.close(s1a)

View File

@ -83,7 +83,7 @@ def _nb_clean(func, *args):
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN): if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
raise raise
else: else:
debug3('%s: err was: %s\n' % (func.__name__, e)) debug3('%s: err was: %s' % (func.__name__, e))
return None return None
@ -111,7 +111,7 @@ class SockWrapper:
def __init__(self, rsock, wsock, connect_to=None, peername=None): def __init__(self, rsock, wsock, connect_to=None, peername=None):
global _swcount global _swcount
_swcount += 1 _swcount += 1
debug3('creating new SockWrapper (%d now exist)\n' % _swcount) debug3('creating new SockWrapper (%d now exist)' % _swcount)
self.exc = None self.exc = None
self.rsock = rsock self.rsock = rsock
self.wsock = wsock self.wsock = wsock
@ -124,9 +124,9 @@ class SockWrapper:
def __del__(self): def __del__(self):
global _swcount global _swcount
_swcount -= 1 _swcount -= 1
debug1('%r: deleting (%d remain)\n' % (self, _swcount)) debug1('%r: deleting (%d remain)' % (self, _swcount))
if self.exc: if self.exc:
debug1('%r: error was: %s\n' % (self, self.exc)) debug1('%r: error was: %s' % (self, self.exc))
def __repr__(self): def __repr__(self):
if self.rsock == self.wsock: if self.rsock == self.wsock:
@ -148,14 +148,14 @@ class SockWrapper:
if not self.connect_to: if not self.connect_to:
return # already connected return # already connected
self.rsock.setblocking(False) 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: try:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux) # connected successfully (Linux)
self.connect_to = None self.connect_to = None
except socket.error: except socket.error:
_, e = sys.exc_info()[:2] _, 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: if e.args[0] == errno.EINVAL:
# this is what happens when you call connect() on a socket # this is what happens when you call connect() on a socket
# that is now connected but returned EINPROGRESS last time, # that is now connected but returned EINPROGRESS last time,
@ -165,7 +165,7 @@ class SockWrapper:
realerr = self.rsock.getsockopt(socket.SOL_SOCKET, realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
socket.SO_ERROR) socket.SO_ERROR)
e = socket.error(realerr, os.strerror(realerr)) 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]: if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet pass # not connected yet
elif e.args[0] == 0: elif e.args[0] == 0:
@ -191,13 +191,13 @@ class SockWrapper:
def noread(self): def noread(self):
if not self.shut_read: if not self.shut_read:
debug2('%r: done reading\n' % self) debug2('%r: done reading' % self)
self.shut_read = True self.shut_read = True
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway # self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
def nowrite(self): def nowrite(self):
if not self.shut_write: if not self.shut_write:
debug2('%r: done writing\n' % self) debug2('%r: done writing' % self)
self.shut_write = True self.shut_write = True
try: try:
self.wsock.shutdown(SHUT_WR) self.wsock.shutdown(SHUT_WR)
@ -218,7 +218,7 @@ class SockWrapper:
except OSError: except OSError:
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
if e.errno == errno.EPIPE: if e.errno == errno.EPIPE:
debug1('%r: uwrite: got EPIPE\n' % self) debug1('%r: uwrite: got EPIPE' % self)
self.nowrite() self.nowrite()
return 0 return 0
else: else:
@ -275,12 +275,12 @@ class Handler:
_add(r, i) _add(r, i)
def callback(self, sock): def callback(self, sock):
log('--no callback defined-- %r\n' % self) log('--no callback defined-- %r' % self)
(r, _, _) = select.select(self.socks, [], [], 0) (r, _, _) = select.select(self.socks, [], [], 0)
for s in r: for s in r:
v = s.recv(4096) v = s.recv(4096)
if not v: if not v:
log('--closed-- %r\n' % self) log('--closed-- %r' % self)
self.socks = [] self.socks = []
self.ok = False self.ok = False
@ -377,7 +377,7 @@ class Mux(Handler):
# for b in self.outbuf: # for b in self.outbuf:
# (s1,s2,c) = struct.unpack('!ccH', b[:4]) # (s1,s2,c) = struct.unpack('!ccH', b[:4])
# ob.append(c) # 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): def send(self, channel, cmd, data):
assert isinstance(data, bytes) assert isinstance(data, bytes)
@ -385,18 +385,18 @@ class Mux(Handler):
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \ p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
+ data + data
self.outbuf.append(p) self.outbuf.append(p)
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n' debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
% (channel, cmd_to_name.get(cmd, hex(cmd)), % (channel, cmd_to_name.get(cmd, hex(cmd)),
len(data), self.fullness)) len(data), self.fullness))
self.fullness += len(data) self.fullness += len(data)
def got_packet(self, channel, cmd, 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))) % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
if cmd == CMD_PING: if cmd == CMD_PING:
self.send(0, CMD_PONG, data) self.send(0, CMD_PONG, data)
elif cmd == CMD_PONG: elif cmd == CMD_PONG:
debug2('received PING response\n') debug2('received PING response')
self.too_full = False self.too_full = False
self.fullness = 0 self.fullness = 0
elif cmd == CMD_EXIT: elif cmd == CMD_EXIT:
@ -431,7 +431,7 @@ class Mux(Handler):
else: else:
callback = self.channels.get(channel) callback = self.channels.get(channel)
if not callback: 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))) % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
else: else:
callback(cmd, data) callback(cmd, data)
@ -446,7 +446,7 @@ class Mux(Handler):
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags) flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
if self.outbuf and self.outbuf[0]: if self.outbuf and self.outbuf[0]:
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0]))) debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0])))
if wrote: if wrote:
self.outbuf[0] = self.outbuf[0][wrote:] self.outbuf[0] = self.outbuf[0][wrote:]
while self.outbuf and not self.outbuf[0]: while self.outbuf and not self.outbuf[0]:
@ -461,11 +461,14 @@ class Mux(Handler):
flags |= os.O_NONBLOCK flags |= os.O_NONBLOCK
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags) flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
try: try:
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE) # 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: except OSError:
_, e = sys.exc_info()[:2] _, e = sys.exc_info()[:2]
raise Fatal('other end: %r' % e) raise Fatal('other end: %r' % e)
# log('<<< %r\n' % b) # log('<<< %r' % b)
if read == b(''): # EOF if read == b(''): # EOF
self.ok = False self.ok = False
if read: if read:
@ -473,7 +476,7 @@ class Mux(Handler):
def handle(self): def handle(self):
self.fill() self.fill()
# log('inbuf is: (%d,%d) %r\n' # log('inbuf is: (%d,%d) %r'
# % (self.want, len(self.inbuf), self.inbuf)) # % (self.want, len(self.inbuf), self.inbuf))
while 1: while 1:
if len(self.inbuf) >= (self.want or HDR_LEN): if len(self.inbuf) >= (self.want or HDR_LEN):
@ -511,7 +514,7 @@ class MuxWrapper(SockWrapper):
self.channel = channel self.channel = channel
self.mux.channels[channel] = self.got_packet self.mux.channels[channel] = self.got_packet
self.socks = [] self.socks = []
debug2('new channel: %d\n' % channel) debug2('new channel: %d' % channel)
def __del__(self): def __del__(self):
self.nowrite() self.nowrite()
@ -527,7 +530,7 @@ class MuxWrapper(SockWrapper):
def setnoread(self): def setnoread(self):
if not self.shut_read: if not self.shut_read:
debug2('%r: done reading\n' % self) debug2('%r: done reading' % self)
self.shut_read = True self.shut_read = True
self.maybe_close() self.maybe_close()
@ -538,13 +541,13 @@ class MuxWrapper(SockWrapper):
def setnowrite(self): def setnowrite(self):
if not self.shut_write: if not self.shut_write:
debug2('%r: done writing\n' % self) debug2('%r: done writing' % self)
self.shut_write = True self.shut_write = True
self.maybe_close() self.maybe_close()
def maybe_close(self): def maybe_close(self):
if self.shut_read and self.shut_write: 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 # remove the mux's reference to us. The python garbage collector
# will then be able to reap our object. # will then be able to reap our object.
self.mux.channels[self.channel] = None self.mux.channels[self.channel] = None
@ -581,9 +584,9 @@ class MuxWrapper(SockWrapper):
def connect_dst(family, ip, port): 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 = socket.socket(family)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
return SockWrapper(outsock, outsock, return SockWrapper(outsock, outsock,
connect_to=(ip, port), connect_to=(ip, port),
peername='%s:%d' % (ip, port)) peername='%s:%d' % (ip, port))
@ -599,11 +602,11 @@ def runonce(handlers, mux):
for s in handlers: for s in handlers:
s.pre_select(r, w, x) s.pre_select(r, w, x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n' debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)'
% (len(handlers), _fds(r), _fds(w), _fds(x), % (len(handlers), _fds(r), _fds(w), _fds(x),
mux.fullness, mux.too_full)) mux.fullness, mux.too_full))
(r, w, x) = select.select(r, w, x) (r, w, x) = select.select(r, w, x)
debug2(' Ready: %d r=%r w=%r x=%r\n' debug2(' Ready: %d r=%r w=%r x=%r'
% (len(handlers), _fds(r), _fds(w), _fds(x))) % (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r + w + x ready = r + w + x
did = {} did = {}

View File

@ -19,9 +19,14 @@ Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
%(user_name)s ALL=NOPASSWD: %(ca)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): def build_config(user_name):
content = template % { content = warning_msg
content += template % {
'ca': command_alias, 'ca': command_alias,
'dist_packages': path_to_dist_packages, 'dist_packages': path_to_dist_packages,
'py': sys.executable, 'py': sys.executable,
@ -42,14 +47,15 @@ def save_config(content, file_name):
process.stdin.write(content.encode()) process.stdin.write(content.encode())
streamdata = process.communicate()[0] streamdata = process.communicate()[0]
sys.stdout.write(streamdata.decode("ASCII"))
returncode = process.returncode returncode = process.returncode
if returncode: if returncode:
log('Failed updating sudoers file.\n') log('Failed updating sudoers file.')
debug1(streamdata) debug1(streamdata)
exit(returncode) exit(returncode)
else: else:
log('Success, sudoers file update.\n') log('Success, sudoers file update.')
exit(0) exit(0)
@ -61,4 +67,5 @@ def sudoers(user_name=None, no_modify=None, file_name=None):
sys.stdout.write(content) sys.stdout.write(content)
exit(0) exit(0)
else: else:
sys.stdout.write(warning_msg)
save_config(content, file_name) save_config(content, file_name)

View File

@ -1,7 +1,7 @@
import io import io
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
from mock import Mock, patch, call from unittest.mock import Mock, patch, call
import sshuttle.firewall import sshuttle.firewall
@ -15,7 +15,7 @@ NSLIST
{inet},1.2.3.33 {inet},1.2.3.33
{inet6},2404:6800:4004:80c::33 {inet6},2404:6800:4004:80c::33
PORTS 1024,1025,1026,1027 PORTS 1024,1025,1026,1027
GO 1 - GO 1 - 0x01 12345
HOST 1.2.3.3,existing HOST 1.2.3.3,existing
""".format(inet=AF_INET, inet6=AF_INET6)) """.format(inet=AF_INET, inet6=AF_INET6))
stdout = Mock() stdout = Mock()
@ -125,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, 64, False, u'2404:6800:4004:80c::', 0, 0),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True, True,
None), None,
'0x01'),
call().setup_firewall( call().setup_firewall(
1025, 1027, 1025, 1027,
[(AF_INET, u'1.2.3.33')], [(AF_INET, u'1.2.3.33')],
@ -133,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, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
True, True,
None), None,
'0x01'),
call().restore_firewall(1024, AF_INET6, True, None), call().restore_firewall(1024, AF_INET6, True, None),
call().restore_firewall(1025, AF_INET, True, None), call().restore_firewall(1025, AF_INET, True, None),
] ]

View File

@ -3,7 +3,7 @@ import socket
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
import errno import errno
from mock import patch, call from unittest.mock import patch, call
import sshuttle.helpers import sshuttle.helpers
@ -24,19 +24,19 @@ def test_log(mock_stderr, mock_stdout):
call.flush(), call.flush(),
] ]
assert mock_stderr.mock_calls == [ assert mock_stderr.mock_calls == [
call.write('prefix: message'), call.write('prefix: message\r\n'),
call.flush(), call.flush(),
call.write('prefix: abc'), call.write('prefix: abc\r\n'),
call.flush(), call.flush(),
call.write('prefix: message 1\n'), call.write('prefix: message 1\r\n'),
call.flush(), call.flush(),
call.write('prefix: message 2\n'), call.write('prefix: message 2\r\n'),
call.write('---> line2\n'), call.write(' line2\r\n'),
call.write('---> line3\n'), call.write(' line3\r\n'),
call.flush(), call.flush(),
call.write('prefix: message 3\n'), call.write('prefix: message 3\r\n'),
call.write('---> line2\n'), call.write(' line2\r\n'),
call.write('---> line3\n'), call.write(' line3\r\n'),
call.flush(), call.flush(),
] ]
@ -51,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout):
call.flush(), call.flush(),
] ]
assert mock_stderr.mock_calls == [ assert mock_stderr.mock_calls == [
call.write('prefix: message'), call.write('prefix: message\r\n'),
call.flush(), call.flush(),
] ]
@ -76,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout):
call.flush(), call.flush(),
] ]
assert mock_stderr.mock_calls == [ assert mock_stderr.mock_calls == [
call.write('prefix: message'), call.write('prefix: message\r\n'),
call.flush(), call.flush(),
] ]
@ -101,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout):
call.flush(), call.flush(),
] ]
assert mock_stderr.mock_calls == [ assert mock_stderr.mock_calls == [
call.write('prefix: message'), call.write('prefix: message\r\n'),
call.flush(), call.flush(),
] ]

View File

@ -3,7 +3,7 @@ from socket import AF_INET, AF_INET6
import struct import struct
import pytest import pytest
from mock import Mock, patch, call from unittest.mock import Mock, patch, call
from sshuttle.helpers import Fatal from sshuttle.helpers import Fatal
from sshuttle.methods import get_method from sshuttle.methods import get_method
@ -11,7 +11,7 @@ from sshuttle.methods import get_method
def test_get_supported_features(): def test_get_supported_features():
method = get_method('nat') method = get_method('nat')
features = method.get_supported_features() features = method.get_supported_features()
assert not features.ipv6 assert features.ipv6
assert not features.udp assert not features.udp
assert features.dns assert features.dns
@ -85,26 +85,52 @@ def test_firewall_command():
@patch('sshuttle.methods.nat.ipt') @patch('sshuttle.methods.nat.ipt')
@patch('sshuttle.methods.nat.ipt_ttl')
@patch('sshuttle.methods.nat.ipt_chain_exists') @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 mock_ipt_chain_exists.return_value = True
method = get_method('nat') method = get_method('nat')
assert method.name == 'nat' assert method.name == 'nat'
with pytest.raises(Exception) as excinfo:
method.setup_firewall(
1024, 1026,
[(AF_INET6, u'2404:6800:4004:80c::33')],
AF_INET6,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True,
None)
assert str(excinfo.value) \
== 'Address family "AF_INET6" unsupported by nat method_name'
assert mock_ipt_chain_exists.mock_calls == [] assert mock_ipt_chain_exists.mock_calls == []
assert mock_ipt_ttl.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)],
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.mock_calls == [] assert mock_ipt.mock_calls == []
with pytest.raises(Exception) as excinfo: with pytest.raises(Exception) as excinfo:
@ -115,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, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
True, True,
None) None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by nat method_name' assert str(excinfo.value) == 'UDP not supported by nat method_name'
assert mock_ipt_chain_exists.mock_calls == [] assert mock_ipt_chain_exists.mock_calls == []
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [] assert mock_ipt.mock_calls == []
method.setup_firewall( method.setup_firewall(
@ -128,14 +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, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
False, False,
None) None,
'0x01')
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'nat', 'sshuttle-1025') call(AF_INET, 'nat', 'sshuttle-1025')
] ]
assert mock_ipt_ttl.mock_calls == [
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'-m', 'ttl', '--ttl', '63')
]
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(AF_INET, 'nat', '-D', 'OUTPUT', '-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', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
@ -146,7 +169,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
'--dest', u'1.2.3.33/32', '-p', 'udp', '--dest', u'1.2.3.33', '-p', 'udp',
'--dport', '53', '--to-ports', '1027'), '--dport', '53', '--to-ports', '1027'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'), '-m', 'addrtype', '--dst-type', 'LOCAL'),
@ -157,20 +180,33 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'--to-ports', '1025') '--to-ports', '1025')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
method.restore_firewall(1025, AF_INET, False, None) method.restore_firewall(1025, AF_INET, False, None)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'nat', 'sshuttle-1025') call(AF_INET, 'nat', 'sshuttle-1025')
] ]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-D', 'OUTPUT', '-j',
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'), 'sshuttle-1025'),
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j',
'sshuttle-1025'),
call(AF_INET, 'nat', '-F', 'sshuttle-1025'), call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
call(AF_INET, 'nat', '-X', 'sshuttle-1025') call(AF_INET, 'nat', '-X', 'sshuttle-1025')
] ]
mock_ipt_chain_exists.reset_mock() 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() mock_ipt.reset_mock()

View File

@ -2,7 +2,7 @@ import socket
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
import pytest import pytest
from mock import Mock, patch, call, ANY from unittest.mock import Mock, patch, call, ANY
from sshuttle.methods import get_method from sshuttle.methods import get_method
from sshuttle.helpers import Fatal, get_env from sshuttle.helpers import Fatal, get_env
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd 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, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False, False,
None) None,
'0x01')
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -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, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)], (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True, True,
None) None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name' assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.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, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)], (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False, False,
None) None,
'0x01')
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -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, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False, False,
None) None,
'0x01')
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s all'), call('-s all'),
@ -330,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, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)], (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True, True,
None) None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name' assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
@ -343,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, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)], (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False, False,
None) None,
'0x01')
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY),
@ -401,7 +407,8 @@ 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, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False, False,
None) None,
'0x01')
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd60441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY),
@ -437,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, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)], (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True, True,
None) None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name' assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
@ -450,7 +458,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, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)], (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False, False,
None) None,
'0x01')
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd60441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY),

View File

@ -1,13 +1,12 @@
import socket import socket
from socket import AF_INET, AF_INET6 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 from sshuttle.methods import get_method
@patch("sshuttle.methods.tproxy.recvmsg") def test_get_supported_features():
def test_get_supported_features_recvmsg(mock_recvmsg):
method = get_method('tproxy') method = get_method('tproxy')
features = method.get_supported_features() features = method.get_supported_features()
assert features.ipv6 assert features.ipv6
@ -15,15 +14,6 @@ def test_get_supported_features_recvmsg(mock_recvmsg):
assert features.dns 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(): def test_get_tcp_dstip():
sock = Mock() sock = Mock()
sock.getsockname.return_value = ('127.0.0.1', 1024) 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')
@patch('sshuttle.methods.tproxy.ipt_ttl')
@patch('sshuttle.methods.tproxy.ipt_chain_exists') @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 mock_ipt_chain_exists.return_value = True
method = get_method('tproxy') method = get_method('tproxy')
assert method.name == '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, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True, True,
None) None,
'0x01')
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET6, 'mangle', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', 'sshuttle-d-1024') call(AF_INET6, 'mangle', 'sshuttle-d-1024')
] ]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-F', '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', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j', call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
'sshuttle-t-1024'), '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', 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-d-1024', '-j', 'ACCEPT'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'), '-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'), '-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32', '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'), '-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', 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', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', 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', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'), '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'), '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '--tproxy-mark', '0x01', '--dest',
u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000', '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
'--on-port', '1024'), '--on-port', '1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'), '-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '--tproxy-mark', '0x01', '--dest',
u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000', '-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
'--on-port', '1024') '--on-port', '1024')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
method.restore_firewall(1025, AF_INET6, True, None) 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-t-1025'),
call(AF_INET6, 'mangle', 'sshuttle-d-1025') call(AF_INET6, 'mangle', 'sshuttle-d-1025')
] ]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET6, 'mangle', '-F', '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') call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
# IPV4 # 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, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)], (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True, True,
None) None,
'0x01')
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', 'sshuttle-t-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', 'sshuttle-d-1025') call(AF_INET, 'mangle', 'sshuttle-d-1025')
] ]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-F', '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', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j', call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
'sshuttle-t-1025'), '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', 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-d-1025', '-j', 'ACCEPT'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'), '-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'), '-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', 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'), '-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', 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'), '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp', '--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', '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
'--dport', '80:80'), '--dport', '80:80'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', 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'), '-m', 'tcp', '-p', 'tcp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', 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'), '-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', 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'), '-m', 'udp', '-p', 'udp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', 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') '-m', 'udp', '-p', 'udp', '--on-port', '1025')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
method.restore_firewall(1025, AF_INET, True, None) 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-t-1025'),
call(AF_INET, 'mangle', 'sshuttle-d-1025') call(AF_INET, 'mangle', 'sshuttle-d-1025')
] ]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-F', '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') call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()

View File

@ -100,3 +100,8 @@ def test_parse_subnetport_ip6_with_mask_and_port():
== [(socket.AF_INET6, ip, 128, 80, 80)] == [(socket.AF_INET6, ip, 128, 80, 80)]
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\ 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,6 @@
import socket import socket
from mock import Mock, patch, call from unittest.mock import Mock, patch, call
import sshuttle.sdnotify import sshuttle.sdnotify

View File

@ -1,7 +1,7 @@
import io import io
import socket import socket
from mock import patch, Mock from unittest.mock import patch, Mock
import sshuttle.server import sshuttle.server

View File

@ -1,7 +1,6 @@
[tox] [tox]
downloadcache = {toxworkdir}/cache/ downloadcache = {toxworkdir}/cache/
envlist = envlist =
py35,
py36, py36,
py37, py37,
py38, py38,