Compare commits

...

418 Commits

Author SHA1 Message Date
dependabot[bot]
5ce4e8c409 build(deps): bump astral-sh/setup-uv from 5 to 6
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-26 06:59:25 +10:00
Vighnesh Pathrikar
934fac9d6c
fix: Updates sudoers config according to executable
- Sudoers config has not been working since the firewall command was updated in 32fceefa.
- This is to update the command for sudoers to keep it similar to what the client executes.
2025-04-05 07:47:57 +11:00
Brian May
e2624f533f chore(master): release 1.3.1 2025-03-26 07:32:24 +11:00
satarsa
375810a9a8
fix: Restore "nft" method
Accidentally removed in refactoring (commit 900acc3).

Closes #1037.

Co-authored-by: Vadim Dyadkin <dyadkin@3lp.cx>
2025-03-26 07:26:38 +11:00
Brian May
5942376090
fix: add pycodestyle config 2025-03-25 12:37:32 +11:00
Brian May
ae3c022d1d
fix: add python lint tools 2025-03-25 12:15:09 +11:00
Brian May
63f94aa6ec
build: fix readthedocs build version number 2025-03-12 08:56:43 +11:00
Brian May
7b662536ba
fix: correct bad version number at runtime 2025-03-12 08:45:22 +11:00
Brian May
cf867248c2
ci: attempt to use dependabot beta support for uv (2)
I can read the instructions. Really!
2025-03-04 07:48:03 +11:00
Brian May
454262829c
ci: attempt to use dependabot beta support for uv
See https://github.com/dependabot/dependabot-core/issues/10478#issuecomment-2691330949
2025-03-03 09:00:56 +11:00
Brian May
684417d363
build: convert from poetry to uv 2025-03-03 09:00:56 +11:00
Brian May
0b7440e65c build: convert from poetry to uv 2025-03-03 08:38:35 +11:00
Brian May
12138e2b8d
build: split build and upload into 2 jobs 2025-03-02 17:18:19 +11:00
Brian May
7991e3d9a2
build: fix pypi upload getting skipped 2025-02-24 08:16:44 +11:00
Brian May
99c4abce81 chore(master): release 1.3.0 2025-02-24 07:54:01 +11:00
Brian May
a2d405a6a7 docs: update installation instructions
* Update pip installation instructions to work
  without setup.py.

* Remove duplication of installation instructions
  in two places.
2025-02-23 20:38:07 +11:00
Christian Schlotter
7fa927ef8c
fix: support ':' sign in password 2025-02-22 08:23:36 +11:00
dependabot[bot]
a1dd6859b0 build(deps-dev): bump flake8 from 7.1.1 to 7.1.2
Bumps [flake8](https://github.com/pycqa/flake8) from 7.1.1 to 7.1.2.
- [Commits](https://github.com/pycqa/flake8/compare/7.1.1...7.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 08:11:24 +11:00
jraylan
8a123d9762
feat: switch to a network namespace on Linux
* Add support to run inside Linux namespace

**Motivation:**
In a specific use case, we use sshuttle to provide access to private
networks from multiple sites to a specific host. The sites may contain
networks that overlap each other, so each site is accessed inside a
different namespace that provides process-level network isolation and
prevents network overlap.

**Objective:**
This commit just adds a convenient way of spawning multiple sshuttle
instances inside different namespaces from a single process, by passing
the namespace's name though the variable --namespace. The result is the
same as calling `ip netns exec $NAMESPACE sshuttle ...`

* Add the argument --namespace-pid

The argument '--namespace-pid' allows sshuttle to attach to the same net
namespace used by a running process.

* PEP-8 compliance

* Add comment

* Make --namespace and --namespace-pid mutually exclusive.

* Prevent UnicodeDecodeError parsing iptables rule with comments

If one or more iptables rule contains a comment with a non-unicode character, an UnicodeDecodeError would be raised.
2025-02-09 08:48:55 +11:00
jraylan
cbe3d1e402
fix: prevent UnicodeDecodeError parsing iptables rule with comments
If one or more iptables rule contains a comment with a non-unicode character, an UnicodeDecodeError would be raised.
2025-02-09 08:45:25 +11:00
Brian May
340ccc705e
docs: replace nix-env with nix-shell 2025-02-08 10:02:31 +11:00
Brian May
1f5e6cea70
fix: remove temp build hack 2025-02-08 09:59:46 +11:00
Brian May
fd6b6bb71f
build: redo publish to pypi 2025-02-08 09:58:09 +11:00
Brian May
5b08caaeb1
build: hack force publish pypi 2025-02-08 09:25:06 +11:00
Brian May
40f6c1d4f2
build: don't skip pypi release 2025-02-08 09:23:33 +11:00
Brian May
c09e2985f2 chore(master): release 1.2.0 2025-02-08 09:21:02 +11:00
Brian May
7725f93d94
build: release to prod pypi 2025-02-08 09:18:45 +11:00
Brian May
75faa9b9e8
build: remove setup.py 2025-02-08 09:16:15 +11:00
Brian May
d910b64be7
feat: Add release-please to build workflow 2025-02-08 08:34:58 +11:00
dependabot[bot]
3f0f88eb09 build(deps): bump abatilo/actions-poetry from 3 to 4
Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 3 to 4.
- [Release notes](https://github.com/abatilo/actions-poetry/releases)
- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc)
- [Commits](https://github.com/abatilo/actions-poetry/compare/v3...v4)

---
updated-dependencies:
- dependency-name: abatilo/actions-poetry
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 07:24:08 +11:00
dependabot[bot]
f7f9a4dbc6 build(deps): bump actions/cache from 3 to 4
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 07:23:55 +11:00
Brian May
bf294643e2
fix: use Python >= 3.10 for docs 2025-02-06 19:11:50 +11:00
Brian May
693ee40c48
fix: ensure poetry works for Python 3.9 2025-02-06 18:57:16 +11:00
Brian May
a0d94367f6
Back out "Bump sphinx from 7.1.2 to 8.1.3"
This backs out commit ac4313decaebd990e535a417d008566213e4516f.
2025-02-06 18:55:48 +11:00
dependabot[bot]
ac4313deca Bump sphinx from 7.1.2 to 8.1.3
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.1.2 to 8.1.3.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.1.2...v8.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 17:42:29 +11:00
Brian May
9bcedf1904
fix: replace requirements.txt files with poetry (4) 2025-02-06 16:48:22 +11:00
Brian May
62da70510e
fix: replace requirements.txt files with poetry (3) 2025-02-06 16:05:55 +11:00
Brian May
d08f78a2d9
fix: replace requirements.txt files with poetry (2) 2025-02-06 16:03:58 +11:00
Brian May
85dc3199a3 fix: replace requirements.txt files with poetry 2025-02-06 15:57:36 +11:00
dependabot[bot]
6f12698209 Bump pytest-cov from 5.0.0 to 6.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.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>
2025-02-06 09:17:17 +11:00
Brian May
4b6f7c6a65
fix: fix broken workflow_dispatch CI rule 2025-02-06 09:11:21 +11:00
Brian May
a3396a443d
fix: Add support for Python 3.11 and Python 3.11 2025-02-06 09:09:50 +11:00
Brian May
339b5221bc
fix: Remove more references to legacy Python versions 2025-02-06 09:08:45 +11:00
Brian May
1084c0f245
fix: drop Python 3.8 support
Python 3.8 support has been dropped upstream.
2025-02-06 09:02:11 +11:00
Brian May
cda60a5233
fix: update nix flake to fix problems 2025-02-06 08:52:31 +11:00
dependabot[bot]
b346e976eb Bump twine from 6.0.1 to 6.1.0
Bumps [twine](https://github.com/pypa/twine) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/6.0.1...6.1.0)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-23 07:54:18 +11:00
dependabot[bot]
7c2b3cd30e Bump pytest from 8.3.3 to 8.3.4
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.3 to 8.3.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/8.3.3...8.3.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-04 07:16:31 +11:00
dependabot[bot]
012fbcb587 Bump twine from 5.1.1 to 6.0.1
Bumps [twine](https://github.com/pypa/twine) from 5.1.1 to 6.0.1.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/v5.1.1...6.0.1)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-03 15:17:45 +11:00
dependabot[bot]
4a1fe0fefe Bump pytest from 8.3.3 to 8.3.4
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.3 to 8.3.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/8.3.3...8.3.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-03 15:17:33 +11:00
dependabot[bot]
6abda35fce Bump pytest-cov from 5.0.0 to 6.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.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>
2024-10-31 07:48:56 +11:00
dependabot[bot]
2f3171670c Bump pytest from 8.3.2 to 8.3.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.2 to 8.3.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/8.3.2...8.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-13 08:39:35 +10:00
dependabot[bot]
304aaa5e46 Bump cryptography from 42.0.3 to 43.0.1
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.3 to 43.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.3...43.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-12 08:17:09 +10:00
dependabot[bot]
f05d6531f2 Bump pytest from 8.3.2 to 8.3.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.2 to 8.3.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/8.3.2...8.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-12 08:16:55 +10:00
Brian May
e6074ed52d Revert "Suppress error P is not recognized as an internal or external command,operable program or batch file."
This reverts commit 6272a0212ccb14b1b32d974e0008cd9e49d526de.
2024-08-20 16:32:50 +10:00
Brian May
ac36a8a20e Revert "add next error log"
This reverts commit dc2287ccf851b27ca556ebccbf3fb241d81faf99.
2024-08-20 16:32:50 +10:00
Brian May
09c3324978 Revert "restore single quote then it looks working"
This reverts commit d1dbed04a0e886945c6391c7ba6a2abee6336fa4.
2024-08-20 16:32:50 +10:00
Brian May
81532b29a9 Revert "remove unnecessary log file"
This reverts commit eaf55ed2960234b44df02135b2bb381a8c59e66e.
2024-08-20 16:32:50 +10:00
o2
eaf55ed296 remove unnecessary log file 2024-08-16 08:46:04 +10:00
o2
d1dbed04a0 restore single quote then it looks working 2024-08-16 08:46:04 +10:00
o2
dc2287ccf8 add next error log 2024-08-16 08:46:04 +10:00
o2
6272a0212c Suppress error P is not recognized as an internal or external command,operable program or batch file. 2024-08-16 08:46:04 +10:00
nicole trinity
8364fd96e8 remove unused imports 2024-08-08 10:35:47 +10:00
Nico T
8da94c39ea transfer work from PR #837 2024-08-08 10:35:47 +10:00
dependabot[bot]
60ee5b910b Bump flake8 from 7.1.0 to 7.1.1
Bumps [flake8](https://github.com/pycqa/flake8) from 7.1.0 to 7.1.1.
- [Commits](https://github.com/pycqa/flake8/compare/7.1.0...7.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-07 09:10:42 +10:00
dependabot[bot]
b9e7a80715 Bump furo from 2024.7.18 to 2024.8.6
Bumps [furo](https://github.com/pradyunsg/furo) from 2024.7.18 to 2024.8.6.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2024.07.18...2024.08.06)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-07 09:10:25 +10:00
nom3ad
bac2a6b0c7 windows: add --remote-shell option to select cmd/powershell 2024-08-06 08:38:24 +10:00
nom3ad
dff6950c4c windows: update docs 2024-08-06 08:38:24 +10:00
nom3ad
df9625bbfd windows: ignore netstat output encoding errors 2024-08-06 08:38:24 +10:00
nom3ad
554b8e3ae5 windows: improve ssnet/try_connect() logic 2024-08-06 08:38:24 +10:00
nom3ad
b826ae6b91 windows: support automatic nameserver detection for --dns option 2024-08-06 08:38:24 +10:00
nom3ad
51287dc4db support server on Windows 2024-08-06 08:38:24 +10:00
nom3ad
ace8642950 add SocketRWShim helper 2024-08-06 08:38:24 +10:00
nom3ad
c4255a23f0 update exec-sshuttle script 2024-08-06 08:38:24 +10:00
nom3ad
6b8e402367 make sure that existing python2 compatibility is not broken by this feature 2024-08-06 08:38:24 +10:00
nom3ad
7a92183f59 windows: better connection tracker 2024-08-06 08:38:24 +10:00
nom3ad
81a598a4cc suppport --auto-hosts in Windows 2024-08-06 08:38:24 +10:00
nom3ad
89a94ff150 support port ranges and exclude subnets 2024-08-06 08:38:24 +10:00
nom3ad
72060abbef code cleanup and small refactoring 2024-08-06 08:38:24 +10:00
nom3ad
de8a19ce69 rename hacks to scripts 2024-08-06 08:38:24 +10:00
nom3ad
32fceefa76 !fix: windows installed script execution 2024-08-06 08:38:24 +10:00
nom3ad
b0799f8752 Fix benchmarking script to use correct node parameter 2024-08-06 08:38:24 +10:00
nom3ad
cd2d69ac08 Bump version to 1.2.0 2024-08-06 08:38:24 +10:00
nom3ad
1885974f52 refactor for future ipv6 support 2024-08-06 08:38:24 +10:00
nom3ad
dadfba488b better windivert filters 2024-08-06 08:38:24 +10:00
nom3ad
8fa15c3ca8 support windivert > 2.0 2024-08-06 08:38:24 +10:00
nom3ad
e19fc01324 !improved windrivert throughput 2024-08-06 08:38:24 +10:00
nom3ad
371258991f Update exec-sshuttle script and related files 2024-08-06 08:38:24 +10:00
nom3ad
db9ec36fac better test-bed scripts 2024-08-06 08:38:24 +10:00
nom3ad
d4d0fa945d fix: bad file descriptor error in windows, fix pytest errors 2024-08-06 08:38:24 +10:00
nom3ad
4a84ad3be6 fix windows CRLF issue on stdin/stdout 2024-08-06 08:38:24 +10:00
nom3ad
900acc3ac7 refactoring to make it better structured 2024-08-06 08:38:24 +10:00
nom3ad
49f46cd528 Add containers based testbed setup 2024-08-06 08:38:24 +10:00
nom3ad
7b8f140870 ensure non loopback address for windivert method 2024-08-06 08:38:24 +10:00
nom3ad
9c5517fd25 use custom RWPair instead of io.BufferedRWPair 2024-08-06 08:38:24 +10:00
nom3ad
3f34e27a2c try not use socket share 2024-08-06 08:38:24 +10:00
nom3ad
2f88fc93cf add some comments 2024-08-06 08:38:24 +10:00
nom3ad
0c4c061123 fix failing tests 2024-08-06 08:38:24 +10:00
nom3ad
482e0cbd00 pass flake8 linting 2024-08-06 08:38:24 +10:00
nom3ad
7da3b024dd fix is_admin_user() helper 2024-08-06 08:38:24 +10:00
nom3ad
b09cc4595b add pydivert as windows specific dependency 2024-08-06 08:38:24 +10:00
nom3ad
c01794f232 windivert: garbage collect timed put connections from tracker 2024-08-06 08:38:24 +10:00
nom3ad
338486930f windivert: add ipv6 support and better thread handling 2024-08-06 08:38:24 +10:00
nom3ad
bd2f960743 more improvements windows support 2024-08-06 08:38:24 +10:00
nom3ad
2c74476124 windivert - basic working connection tracker 2024-08-06 08:38:24 +10:00
nom3ad
5a64c81b5b experimental windows method 2024-08-06 08:38:24 +10:00
dependabot[bot]
2408563f3b Bump flake8 from 7.1.0 to 7.1.1
Bumps [flake8](https://github.com/pycqa/flake8) from 7.1.0 to 7.1.1.
- [Commits](https://github.com/pycqa/flake8/compare/7.1.0...7.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-06 08:36:18 +10:00
dependabot[bot]
834ac02a5d Bump pytest from 8.3.1 to 8.3.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.1 to 8.3.2.
- [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/8.3.1...8.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-27 08:35:10 +10:00
dependabot[bot]
bd3164db22 Bump pytest from 8.3.1 to 8.3.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.1 to 8.3.2.
- [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/8.3.1...8.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-26 08:44:19 +10:00
dependabot[bot]
037ee9025e Bump pytest from 8.2.2 to 8.3.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.2 to 8.3.1.
- [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/8.2.2...8.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-25 07:44:51 +10:00
dependabot[bot]
bf2db72393 Bump pytest from 8.2.2 to 8.3.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.2 to 8.3.1.
- [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/8.2.2...8.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-24 19:52:22 +10:00
dependabot[bot]
f10535edf4 Bump furo from 2024.5.6 to 2024.7.18
Bumps [furo](https://github.com/pradyunsg/furo) from 2024.5.6 to 2024.7.18.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2024.05.06...2024.07.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-24 19:45:38 +10:00
Antoine Jacoutot
fd63611b5a Fix pf_rule size in OpenBSD. 2024-07-12 09:52:57 +10:00
Antoine Jacoutot
9c3107bed7 Unbreak OpenBSD runtime.
sizeof(struct pfioc_rule) changed in recent OpenBSD releases.
This fixes the ioctl call to DIOCCHANGERULE.
2024-07-12 09:52:57 +10:00
dependabot[bot]
fdcc840b7b Bump zipp from 3.17.0 to 3.19.1
Bumps [zipp](https://github.com/jaraco/zipp) from 3.17.0 to 3.19.1.
- [Release notes](https://github.com/jaraco/zipp/releases)
- [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst)
- [Commits](https://github.com/jaraco/zipp/compare/v3.17.0...v3.19.1)

---
updated-dependencies:
- dependency-name: zipp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-10 15:25:02 +10:00
Samir Aguiar
348f0eb653 Add support for non-compliant ssh wrappers
ssh wrappers like teleport's tsh do not correctly interpret the
double dash as an argument delimiter and will not work properly
with sshuttle. This PR adds a new command line switch to handle
these cases by not adding the delimiter.

Fixes #599
2024-07-07 13:28:26 +10:00
dependabot[bot]
6cdae8c3e5 Bump certifi from 2024.2.2 to 2024.7.4
Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-07 13:27:24 +10:00
dependabot[bot]
bdf2797b74 Bump twine from 5.1.0 to 5.1.1
Bumps [twine](https://github.com/pypa/twine) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/5.1.0...v5.1.1)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-28 09:25:28 +10:00
dependabot[bot]
cc38cc2def Bump flake8 from 7.0.0 to 7.1.0
Bumps [flake8](https://github.com/pycqa/flake8) from 7.0.0 to 7.1.0.
- [Commits](https://github.com/pycqa/flake8/compare/7.0.0...7.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 09:50:04 +10:00
dependabot[bot]
4ccf528664 Bump urllib3 from 2.2.1 to 2.2.2
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 09:34:15 +10:00
dependabot[bot]
83c136d6e6 Bump flake8 from 7.0.0 to 7.1.0
Bumps [flake8](https://github.com/pycqa/flake8) from 7.0.0 to 7.1.0.
- [Commits](https://github.com/pycqa/flake8/compare/7.0.0...7.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 09:33:53 +10:00
dependabot[bot]
efbc4d066f Bump pytest from 8.2.1 to 8.2.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.1 to 8.2.2.
- [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/8.2.1...8.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-07 08:49:18 +10:00
dependabot[bot]
a0f466a07c Bump pytest from 8.2.1 to 8.2.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.1 to 8.2.2.
- [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/8.2.1...8.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-06 10:42:41 +10:00
dependabot[bot]
d660d8159b ---
updated-dependencies:
- dependency-name: requests
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 20:43:42 +10:00
dependabot[bot]
8d5e23477e ---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-22 20:43:28 +10:00
dependabot[bot]
a91e0c0470 Bump pytest from 8.2.0 to 8.2.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.0 to 8.2.1.
- [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/8.2.0...8.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-21 16:00:12 +10:00
dependabot[bot]
c0938bc9a5 Bump twine from 5.0.0 to 5.1.0
Bumps [twine](https://github.com/pypa/twine) from 5.0.0 to 5.1.0.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/5.0.0...5.1.0)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-18 09:35:51 +10:00
dependabot[bot]
975d208d60 Bump furo from 2024.4.27 to 2024.5.6
Bumps [furo](https://github.com/pradyunsg/furo) from 2024.4.27 to 2024.5.6.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2024.04.27...2024.05.06)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-08 09:03:54 +10:00
dependabot[bot]
39a7b1b47f Bump pytest from 8.1.1 to 8.2.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0.
- [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/8.1.1...8.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 11:55:12 +10:00
dependabot[bot]
4ba7612d90 Bump furo from 2024.1.29 to 2024.4.27
Bumps [furo](https://github.com/pradyunsg/furo) from 2024.1.29 to 2024.4.27.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2024.01.29...2024.04.27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-30 08:47:03 +10:00
dependabot[bot]
ea0559eaea Bump pytest from 8.1.1 to 8.2.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0.
- [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/8.1.1...8.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-30 08:46:52 +10:00
dependabot[bot]
6bd3bd738a Bump idna from 3.6 to 3.7
Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-12 14:53:36 +10:00
Brian May
116b1e22b1 Revert "Bump cryptography from 42.0.3 to 42.0.4"
This reverts commit 87bd34e09422b9dba70b83d20ff721f75481e8c2.

Fixes #939

This was to be fixed in
https://github.com/nix-community/poetry2nix/pull/1538, but merging that
is taking longer then I might have hoped.
2024-04-08 09:03:21 +10:00
dependabot[bot]
694a9c8a5b Bump pytest-cov from 4.1.0 to 5.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.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>
2024-03-27 09:05:30 +11:00
dependabot[bot]
264e4d94b8 Bump pytest-cov from 4.1.0 to 5.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.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>
2024-03-26 11:52:00 +11:00
dependabot[bot]
afbdf8b606 Bump pytest from 8.0.2 to 8.1.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.2 to 8.1.1.
- [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/8.0.2...8.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-13 09:26:29 +11:00
dependabot[bot]
9a4df1fdcf Bump pytest from 8.0.2 to 8.1.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.2 to 8.1.1.
- [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/8.0.2...8.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-12 09:49:32 +11:00
dependabot[bot]
3d875b8ca8 Bump pytest from 8.0.2 to 8.1.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.2 to 8.1.0.
- [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/8.0.2...8.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-05 09:18:21 +11:00
dependabot[bot]
313ada3ff7 Bump pytest from 8.0.1 to 8.0.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.1 to 8.0.2.
- [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/8.0.1...8.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-28 09:11:53 +11:00
dependabot[bot]
934618b603 Bump pytest from 8.0.1 to 8.0.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.1 to 8.0.2.
- [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/8.0.1...8.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-27 08:52:55 +11:00
dependabot[bot]
87bd34e094 Bump cryptography from 42.0.3 to 42.0.4
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.3 to 42.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.3...42.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-22 15:22:26 +11:00
dependabot[bot]
83debdfb21 Bump pytest from 8.0.0 to 8.0.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.0 to 8.0.1.
- [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/8.0.0...8.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-20 08:56:07 +11:00
Brian May
fd424c5c55 Bump version: 1.1.1 → 1.1.2 2024-02-19 11:21:39 +11:00
Brian May
e6563f2c39 Add twine to poetry packages 2024-02-19 11:21:31 +11:00
Brian May
dd037dd8ef Add experimental peotry and nix flake stuff 2024-02-19 11:21:31 +11:00
Pouria Mousavizadeh Tehrani
89bd3fc2f3 Update FreeBSD Installation README.rst 2024-01-31 19:28:53 +11:00
Christian Clauss
5c479220a7 Update usage.rst 2024-01-31 19:28:21 +11:00
Christian Clauss
32d0054455 Fix typos discovered by codespell
https://pypi.org/project/codespell
2024-01-31 19:28:21 +11:00
Christian Clauss
b2a29d3b22 Allow flake8 to determine the version of pyflakes 2024-01-31 19:27:58 +11:00
dependabot[bot]
9b831499d7 Bump furo from 2023.9.10 to 2024.1.29
Bumps [furo](https://github.com/pradyunsg/furo) from 2023.9.10 to 2024.1.29.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2023.09.10...2024.01.29)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-31 19:27:14 +11:00
Benjamin Barthe
e4ae714cf8 fixing a tiny typo 2024-01-31 14:35:02 +11:00
dependabot[bot]
152c14c079 Bump pytest from 7.4.4 to 8.0.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.4 to 8.0.0.
- [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/7.4.4...8.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-31 14:26:54 +11:00
Scott Kuhl
a604d107ef Keep terminal in a sane state when sudo use_pty is used.
This fixes #909 and is an alternative to the #922 pull request. When
sudo's use_pty is used with sshuttle, it causes issues with the
terminal. Pull request #712 contains some fixes for this problem.
However, when sshuttle is run with the --daemon option, it left the
user's terminal in a non-sane state. The problem appears to be related
to a socketpair that the firewall uses for communication. By setting
it up slightly differently (see changes to client.py and firewall.py),
the terminal state is no longer disrupted. This commit also changes
line endings of the printed messages from \r\n to \n. This undoes a
change introduced by pull request #712 and is no longer needed.
2024-01-05 19:08:34 +11:00
Scott Kuhl
b4e4680ef4 Workaround when sudo prints text to standard out
When we use sudo and start the firewall process, we should be able to
read standard in and find the string "READY". However, some
administrators use a wrapper around sudo to print warning messages
(instead of sudo's lecture feature) to standard out. This commit reads
up to 100 lines looking for "READY" instead of expecting it on the
first line.

I believe this should fix issue #916.
2024-01-02 09:08:09 +11:00
dependabot[bot]
59b6777f01 Bump pytest from 7.4.3 to 7.4.4
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.3 to 7.4.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/7.4.3...7.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-02 09:06:36 +11:00
dependabot[bot]
ef804e7cdb Bump github/codeql-action from 2 to 3
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 09:17:15 +11:00
dependabot[bot]
67b4499c52 Bump actions/setup-python from 4 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-08 08:38:47 +11:00
dependabot[bot]
e53c0df411 Bump pytest from 7.4.2 to 7.4.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.2 to 7.4.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/7.4.2...7.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 08:54:09 +11:00
Jose M Perez
794b14eaac tproxy: Apply DNS rules first
Having --dst-type LOCAL rules before DNS ones forces the usage of a
dnsmasq-like program to retrigger DNS requests directed locally
because they are fast-tracked through the firewall and ignored by
sshuttle.

As dns options documentation state that they capture the requests no
matter the server, and other methods and older versions behave
consistently, change the iptables rules to apply DNS ones first.
2023-10-04 08:11:52 +11:00
dependabot[bot]
670cc363ba Bump furo from 2023.8.19 to 2023.9.10
Bumps [furo](https://github.com/pradyunsg/furo) from 2023.8.19 to 2023.9.10.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2023.08.19...2023.09.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 20:52:43 +10:00
dependabot[bot]
6f70519dc1 Bump pyflakes from 2.5.0 to 3.1.0
Bumps [pyflakes](https://github.com/PyCQA/pyflakes) from 2.5.0 to 3.1.0.
- [Changelog](https://github.com/PyCQA/pyflakes/blob/main/NEWS.rst)
- [Commits](https://github.com/PyCQA/pyflakes/compare/2.5.0...3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-09 12:11:54 +10:00
dependabot[bot]
efb7d1f6cc Bump flake8 from 5.0.4 to 6.1.0
Bumps [flake8](https://github.com/pycqa/flake8) from 5.0.4 to 6.1.0.
- [Commits](https://github.com/pycqa/flake8/compare/5.0.4...6.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-09 12:10:57 +10:00
dependabot[bot]
031fb4d053 Bump pytest from 7.4.1 to 7.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.1 to 7.4.2.
- [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/7.4.1...7.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-09 12:08:29 +10:00
dependabot[bot]
3e80464626 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 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/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-06 01:47:00 +00:00
dependabot[bot]
399d389af6 Bump pytest from 7.4.0 to 7.4.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.0 to 7.4.1.
- [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/7.4.0...7.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 22:20:48 +00:00
dependabot[bot]
c2ddaa0bcf Bump furo from 2023.8.17 to 2023.8.19
Bumps [furo](https://github.com/pradyunsg/furo) from 2023.8.17 to 2023.8.19.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2023.08.17...2023.08.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-22 13:48:45 +10:00
dependabot[bot]
cec87a5341 Bump furo from 2023.7.26 to 2023.8.17
Bumps [furo](https://github.com/pradyunsg/furo) from 2023.7.26 to 2023.8.17.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2023.07.26...2023.08.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-19 11:57:55 +10:00
Alex Jurkiewicz
0ddebdeee6 Add support for SSHUTTLE_ARGS environment variable 2023-08-09 15:06:05 +10:00
Alexander Naumov
3c3f5de672 sshuttle is also avaliable on OpenBSD 2023-08-08 08:19:46 +10:00
Fata Nugraha
9f718e8632 Fix typo 2023-08-07 20:00:32 +10:00
Fata Nugraha
3abc3d2a1a Fix lint issues 2023-08-07 20:00:32 +10:00
Fata Nugraha
5b9f438d42 Fix tests 2023-08-07 20:00:32 +10:00
Fata Nugraha
998e5c5849 Fix tests 2023-08-07 20:00:32 +10:00
Fata Nugraha
7c140daf07 Pass group to firewall 2023-08-07 20:00:32 +10:00
Fata Nugraha
755e522eff Allow user to tunnel traffic to local port 2023-08-07 20:00:32 +10:00
Fata Nugraha
6b7cf80420 Add support for group-based routing 2023-08-07 20:00:32 +10:00
dependabot[bot]
ac06e7968f Bump sphinx from 7.1.1 to 7.1.2
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 7.1.1 to 7.1.2.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.1.1...v7.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-03 08:29:14 +10:00
dependabot[bot]
f597e70ae6 Bump furo from 2023.5.20 to 2023.7.26
Bumps [furo](https://github.com/pradyunsg/furo) from 2023.5.20 to 2023.7.26.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2023.05.20...2023.07.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 08:11:14 +10:00
Brian May
802c6f5a6e Use furo style for docs
The default read the docs theme does not work with the latest Sphinx.
2023-07-31 08:18:18 +10:00
dependabot[bot]
17bfdc24b8 Bump sphinx from 6.2.1 to 7.1.1
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.1 to 7.1.1.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.1...v7.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-31 08:10:15 +10:00
dependabot[bot]
4e592265f6 Bump pytest from 7.3.2 to 7.4.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.2 to 7.4.0.
- [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/7.3.2...7.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 11:15:44 +00:00
dependabot[bot]
a289580f24 Bump pytest from 7.3.1 to 7.3.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.1 to 7.3.2.
- [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/7.3.1...7.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-12 22:27:29 +00:00
dependabot[bot]
799c9f33d0 Bump pytest-cov from 4.0.0 to 4.1.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.0.0 to 4.1.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.0.0...v4.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-25 22:00:02 +00:00
Brian May
5778437148 Revert "Bump sphinx from 6.2.1 to 7.0.0"
This reverts commit dffc1c7f929d2a41ef84929318537ef41352ba01.
2023-05-03 10:04:09 +10:00
dependabot[bot]
dffc1c7f92 Bump sphinx from 6.2.1 to 7.0.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.1 to 7.0.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.1...v7.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-02 09:33:10 +10:00
dependabot[bot]
25cd95130d Bump sphinx from 6.2.0 to 6.2.1
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.0...v6.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-27 11:06:41 +10:00
dependabot[bot]
a54fd8ab4e Bump sphinx from 6.1.3 to 6.2.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.1.3 to 6.2.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.1.3...v6.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 21:05:51 +10:00
dependabot[bot]
d336002833 Bump pytest from 7.3.0 to 7.3.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.0 to 7.3.1.
- [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/7.3.0...7.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-18 13:29:42 +10:00
dependabot[bot]
fd8a0b624d Bump pytest from 7.2.2 to 7.3.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.2 to 7.3.0.
- [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/7.2.2...7.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-10 21:11:21 +10:00
dependabot[bot]
e0ef2964cd Bump pytest from 7.2.1 to 7.2.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.1 to 7.2.2.
- [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/7.2.1...7.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-07 09:07:37 +11:00
dependabot[bot]
faf34e14e0 Bump pytest from 7.2.0 to 7.2.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.0 to 7.2.1.
- [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/7.2.0...7.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-17 08:49:37 +11:00
dependabot[bot]
23207f27fa Bump sphinx from 6.1.2 to 6.1.3
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.1.2...v6.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-11 21:09:13 +11:00
dependabot[bot]
7edc7ba7bc Bump sphinx from 6.1.1 to 6.1.2
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.1.1...v6.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-11 09:38:37 +11:00
dependabot[bot]
8ba8dff719 Bump sphinx from 6.0.0 to 6.1.1
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.0.0 to 6.1.1.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/v6.1.1/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.0.0...v6.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-07 09:19:07 +11:00
dependabot[bot]
57111d7a13 Bump sphinx from 5.3.0 to 6.0.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.3.0 to 6.0.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.3.0...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-31 09:26:55 +11:00
rmchale
f23b24b74e Update ssyslog.py 2022-12-13 07:43:54 +11:00
dinosaurtirex
b8e6ebf741 Removed a little bit of legacy code
Removed a few lines of legacy code (to make it look more clean)
2022-11-28 11:44:41 +11:00
dependabot[bot]
53da036879 Bump pytest from 7.1.3 to 7.2.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.3 to 7.2.0.
- [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/7.1.3...7.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-27 08:20:31 +11:00
dependabot[bot]
ad05994e65 Bump sphinx from 5.2.3 to 5.3.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.3 to 5.3.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.3...v5.3.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-10-18 07:38:00 +11:00
dependabot[bot]
e704ea74e5 Bump sphinx from 5.2.2 to 5.2.3
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.2...v5.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-04 08:06:40 +11:00
dependabot[bot]
d99940c58e Bump pytest-cov from 3.0.0 to 4.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 3.0.0 to 4.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/v3.0.0...v4.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>
2022-09-30 07:46:34 +10:00
dependabot[bot]
1d240e0cd9 Bump sphinx from 5.2.1 to 5.2.2
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.1...v5.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-28 20:31:42 +10:00
dependabot[bot]
060f849c7e Bump sphinx from 5.1.1 to 5.2.1
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.1.1 to 5.2.1.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.1...v5.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-27 07:48:49 +10:00
Brian May
9df7a0a053 Bump version: 1.1.0 → 1.1.1 2022-09-06 08:17:47 +10:00
Brian May
a28c8ae10b Include version in setup.py too 2022-09-06 08:17:36 +10:00
Brian May
4f4d6d9f4d Add ASDF .tool-versions file 2022-09-06 08:06:34 +10:00
Brian May
a1c7e64b0e Add .coverage to .gitignore 2022-09-06 08:04:28 +10:00
dependabot[bot]
88139ed2e5 Bump pytest from 7.1.2 to 7.1.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.2 to 7.1.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/7.1.2...7.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-06 07:53:59 +10:00
Bastian Venthur
810b4a3170 added bump2version 2022-09-05 08:24:31 +10:00
Bastian Venthur
98233530a0 remove setuptools-scm 2022-09-05 08:24:31 +10:00
Bastian Venthur
77eb8167c4 all elements are strings 2022-08-29 19:52:50 +10:00
Bastian Venthur
a6efc6b653 This test broke in Python3.11
Fixed the test and only check for instance: str for families that are
not explicitly covered in `family_to_str`

closes: #784
2022-08-29 19:52:10 +10:00
Brian May
f8086dfa59 Update flake8 and pyflakes 2022-08-05 08:00:56 +10:00
dependabot[bot]
58d72a93d2 Bump sphinx from 5.1.0 to 5.1.1
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.0...v5.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-27 21:02:54 +10:00
dependabot[bot]
6929b79274 Bump sphinx from 5.0.2 to 5.1.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.2 to 5.1.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.2...v5.1.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-07-26 08:20:09 +10:00
dependabot[bot]
bf4fa6cacc Bump setuptools-scm from 7.0.4 to 7.0.5
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.4 to 7.0.5.
- [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/v7.0.4...v7.0.5)

---
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-07-13 07:47:27 +10:00
dependabot[bot]
2462d6d204 Bump setuptools-scm from 7.0.3 to 7.0.4
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.3 to 7.0.4.
- [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/v7.0.3...v7.0.4)

---
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-07-04 20:30:26 +10:00
dependabot[bot]
86c69dda48 Bump setuptools-scm from 7.0.2 to 7.0.3
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.2 to 7.0.3.
- [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/v7.0.2...v7.0.3)

---
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-06-30 20:31:48 +10:00
Scott Kuhl
df98790206 Fix incorrect permissions for /etc/hosts
If we modify /etc/hosts, we read/copy the ownership and permissions
from the existing /etc/hosts before we make our new temporary file
which will eventually overwrite /etc/hosts. If we fail to retrieve the
permissions of the existing /etc/hosts file, we made the temporary
file owned by root 0o600 permissions. It should have 0o644 permissions
so that /etc/hosts has the correct permissions once we rename it.

It is unlikely many encoutered this bug since most machines have
/etc/hosts prior to sshuttle running and we should be able to read the
permission/ownership of that existing file.
2022-06-26 09:36:13 +10:00
dependabot[bot]
f9a9dad9ff Bump setuptools-scm from 7.0.1 to 7.0.2
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 7.0.1 to 7.0.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/v7.0.1...v7.0.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-06-24 08:21:27 +10:00
dependabot[bot]
1fa47bf8e1 Bump setuptools-scm from 6.4.2 to 7.0.1
Bumps [setuptools-scm](https://github.com/pypa/setuptools_scm) from 6.4.2 to 7.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/v6.4.2...v7.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-23 08:24:03 +10:00
dependabot[bot]
7525f8d4c5 Bump sphinx from 5.0.1 to 5.0.2
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.1...v5.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-19 09:28:58 +10:00
Bastian Venthur
a33a4829e2 fixed some spelling mistakes 2022-06-15 07:43:25 +10:00
dependabot[bot]
90ec0a9cb6 Bump actions/setup-python from 3 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-09 20:23:49 +10:00
dependabot[bot]
0914bef9a2 Bump sphinx from 5.0.0 to 5.0.1
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.0.0...v5.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-05 09:41:39 +10:00
Nikos Atlas
93200f7095 add comment and warning 2022-06-03 07:48:44 +10:00
Nikos Atlas
1def53e085 fallback to file editing in case file is locked 2022-06-03 07:48:44 +10:00
dependabot[bot]
553bc2b70c Bump sphinx from 4.5.0 to 5.0.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.5.0 to 5.0.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.5.0...v5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-30 20:31:28 +10:00
dependabot[bot]
bf4cb64f25 Bump pytest from 7.1.1 to 7.1.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.1 to 7.1.2.
- [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/7.1.1...7.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 20:44:11 +10:00
Brian May
004365f5c7 Delete stresstest.py
This file has not been touched in years. And looks like it is broken,
e.g. listener.accept() is called after infinite loop.
2022-04-24 18:38:51 +10:00
Brian May
d6fa0c1462 Replace BaseException with Exception
BaseException includes exceptions like SystemExit, KeyboardInterrupt
and GeneratorExit that we should not be catching.
2022-04-24 17:50:05 +10:00
Brian May
9e3209e931 Remove unused flags assignment 2022-04-24 17:40:43 +10:00
Brian May
7d67231faf Update style issues 2022-04-24 17:37:10 +10:00
Brian May
0b267cdeff
Create codeql.yml 2022-04-24 17:00:57 +10:00
Brian May
30cdc5e74b Fix LGTM reported issues 2022-04-24 16:43:07 +10:00
Brian May
181bf648a7 Remove useless assignment 2022-04-24 16:11:14 +10:00
Brian May
10341f3ad6 Add missing raise keyword for UDP not supported error 2022-04-24 16:09:55 +10:00
dependabot[bot]
6f92bd8ccf Bump sphinx from 4.3.2 to 4.5.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.2 to 4.5.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.5.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-03-29 11:51:12 +11:00
dependabot[bot]
a7ca6d47a6 Bump pytest from 7.0.1 to 7.1.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.1.
- [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/7.0.1...7.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-29 11:50:51 +11:00
Brian May
6d36916f48 Remove support for Python 3.6 and 3.7
Fixes #716
2022-03-29 11:47:05 +11:00
Scott Kuhl
5719d424de Remove --sudoers, improve --sudoers-no-modify
Allowing sshuttle to add/overwrite sudoers configuration file at
locations of the users' choosing adds complexity to the code compared
to asking users to install the sudo configuration themselves. It
requires sshuttle to make decisions about how much effort we put into
ensuring that the file is written to a proper location. The current
method relies on the 'realpath' program which is not installed on
MacOS by default.

There are serious problems when the sudo configuration is used to
allow a user to *only* run sshuttle as root (with or without a
password). First, that user could then use the --sudoers option to
give other users sudo privileges. Second, the user can run any command
as root because sshuttle accepts a --ssh-cmd parameter which allows a
user to specify a program that sshuttle should run. There may also be
additional issues that we have not identified.

By removing the --sudoers option (and the associated sudoers-add
script), this reduces the problems above. This code keeps the
--sudoers-no-modify feature which prints a configuration to stdout for
the user to install. It includes a clear warning about how --ssh-cmd
could potentially be abused to run other programs.

A warning about some of these issues has been in sshuttle since
version 1.1.0. This commit also adds that warning to more locations in
the documentation.
2022-03-13 09:29:56 +11:00
lbausch
9431bb7a2f Fix typo 2022-03-03 07:28:46 +11:00
Brian May
8c94b55d30
Merge pull request #743 from sshuttle/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2.4.0 to 3
2022-03-03 07:28:13 +11:00
dependabot[bot]
1ed09fbe72
Bump actions/checkout from 2.4.0 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.
- [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.4.0...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 10:10:13 +00:00
Brian May
ce7b4f83b2
Merge pull request #741 from sshuttle/dependabot/github_actions/actions/setup-python-3
Bump actions/setup-python from 2.3.2 to 3
2022-03-02 09:30:19 +11:00
dependabot[bot]
d9d3533b82
Bump actions/setup-python from 2.3.2 to 3
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.3.2 to 3.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2.3.2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 10:10:14 +00:00
Brian May
0932bdd231
Merge pull request #737 from sshuttle/dependabot/pip/pytest-7.0.1
Bump pytest from 7.0.0 to 7.0.1
2022-02-15 07:34:15 +11:00
dependabot[bot]
f4150b7283
Bump pytest from 7.0.0 to 7.0.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.0 to 7.0.1.
- [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/7.0.0...7.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 10:13:35 +00:00
Brian May
bfd6f5d088
Merge pull request #735 from mangano-ito/allows-wildcard-hosts
Allows wildcard host names as subnets
2022-02-11 08:10:46 +11:00
mangano-ito
016919cf95 accept a wildcarded host 2022-02-10 23:32:43 +09:00
mangano-ito
48ab82b81e test a wildcarded host acceptable 2022-02-10 23:32:43 +09:00
Brian May
d8a07a5244
Merge pull request #734 from mangano-ito/add-tests-for-hostname-resolution
Add tests for host name resolution
2022-02-10 20:12:56 +11:00
mangano-ito
2f5c946b48 define flake8 max line length longer (79 to 128) 2022-02-10 08:41:22 +09:00
mangano-ito
1d4c059f44 format styles: E251 unexpected spaces around keyword / parameter equals (flake8) 2022-02-10 08:41:22 +09:00
mangano-ito
b9b89c3f55 add another example for host resolution tests 2022-02-09 21:29:24 +09:00
mangano-ito
e5eb5afef0 use mocked getaddrinfo to make host name resolution stable 2022-02-09 21:29:24 +09:00
mangano-ito
19e2a1810d add getaddrinfo mock for test-cases with hosts 2022-02-09 21:29:24 +09:00
mangano-ito
2f026c84af test hosts with port specified 2022-02-09 21:29:24 +09:00
mangano-ito
04214eaf89 test hosts with no port specified 2022-02-09 21:29:24 +09:00
Brian May
6b07cb2d21
Merge pull request #731 from sshuttle/dependabot/pip/pytest-7.0.0
Bump pytest from 6.2.5 to 7.0.0
2022-02-08 07:59:37 +11:00
Brian May
b1aa5fef89
Merge pull request #730 from sshuttle/dependabot/github_actions/actions/setup-python-2.3.2
Bump actions/setup-python from 2.3.1 to 2.3.2
2022-02-08 07:59:21 +11:00
dependabot[bot]
d378cbd582
Bump pytest from 6.2.5 to 7.0.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.0.
- [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.5...7.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 10:13:04 +00:00
dependabot[bot]
166e4d6742
Bump actions/setup-python from 2.3.1 to 2.3.2
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2.3.1...v2.3.2)

---
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>
2022-02-07 10:12:58 +00:00
Brian May
317211a974
Merge pull request #728 from skuhl/ipv6-bind-error-msg
Improve message when bind fails with a IPv6 address
2022-02-05 08:57:02 +11:00
Brian May
c28976a10e
Merge pull request #729 from skuhl/man-page-disable-ipv6
Clarify --disable-ipv6 in man page.
2022-02-05 08:51:54 +11:00
Scott Kuhl
09c534bcf3 Clarify --disable-ipv6 in man page.
The description for --disable-ipv6 did not list all methods that
support IPv6.
2022-02-04 15:27:48 -05:00
Scott Kuhl
0c3b615736 Improve message when bind fails with a IPv6 address
The comments at the end of issue #673 shows an example where sshuttle
exits with an OSError exception when it cannot bind to an IPv6
address. This patch makes a suggestion to try the --disable-ipv6
option instead of the cryptic error message.
2022-02-04 15:20:25 -05:00
Brian May
c783fdb472
Merge pull request #727 from skuhl/fix-sudoers-in-1.1.0
Make --sudoers option work properly, fix regression in v1.1.0
2022-02-04 09:22:29 +11:00
Scott Kuhl
0f92735ee5 Make --sudoers option work properly, fix regression in v1.1.0
Commit d6f75fa unintentionally changed the order of some of the
parameters when running the firewall process. This prevented the
--sudoers option from working properly. This patch restores the
previous ordering.

Most discussion was in issue #724. Also fixes #722 and #723.
2022-02-03 13:53:39 -05:00
Brian May
3d51bcba95 Move release notes to github 2022-01-28 09:27:47 +11:00
Brian May
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
Brian May
d23a0fd2c5
Revert "Bump sphinx from 4.3.2 to 4.4.0" 2022-01-22 09:44:59 +11:00
Brian May
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
4e43af758d
Merge pull request #708 from skuhl/doas
Allow use of sudo or doas.
2022-01-17 08:04:50 +11:00
Brian May
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
Brian May
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
Scott Kuhl
80a822e079 Fix flake8 and unit test errors introduced by use_pty fixes. 2022-01-07 13:21:16 -05:00
Scott Kuhl
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
Scott Kuhl
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
Scott Kuhl
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
Scott Kuhl
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
Brian May
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
Scott Kuhl
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
Brian May
a2776cbbd9 Upgrade Sphinx version 2022-01-04 11:47:35 +11:00
Brian May
44b772d049 Add readthedocs config 2022-01-04 11:38:07 +11:00
Brian May
ae1faa7fa1
Merge pull request #709 from skuhl/tproxy-doc-fix
Minor improvement to tproxy documentation.
2022-01-02 12:02:33 +11:00
Scott Kuhl
175da40db7 Fix typo in tproxy documentation.
"IPv6 DNS" was listed twice.
2021-12-31 14:54:17 -05:00
Scott Kuhl
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
Scott Kuhl
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
Brian May
354cbe6071
Merge pull request #705 from JohnHay/ipfw
Make ipfw method work
2021-12-24 10:10:02 +11:00
JohnHay
922d827948 Flake8 does not like if we go over 79 characters on a line. 2021-12-22 06:04:25 +02:00
JohnHay
e6f076e1a5 Make flake8 happy. 2021-12-21 16:12:11 +02:00
JohnHay
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
JohnHay
0cfee0ba84 ipfw expects text. 2021-12-20 09:20:32 +02:00
JohnHay
f247853f39 Changed one place where the Exception text was still tproxy to ipfw. 2021-12-20 08:58:25 +02:00
JohnHay
7c1f2b08cf Indent the else to the same level as its if. 2021-12-20 08:55:38 +02:00
JohnHay
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
JohnHay
b1ee4f1d65 check-state does not take extra options. 2021-12-20 08:37:15 +02:00
Brian May
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
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
Brian May
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
dependabot[bot]
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
Brian May
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
Christian Clauss
3de631abec
pythonpackage.yml: Add Python 3.10 to the testing 2021-11-01 19:35:57 +01:00
Brian May
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
9399cf10f6
Merge pull request #682 from a1346054/fixes
Simple maintenance improvements
2021-09-23 08:49:18 +10:00
a1346054
b95c1f3357
Trim excess whitespace 2021-09-22 12:36:36 +00:00
a1346054
5257d9fd1b
Fix shellcheck warnings 2021-09-22 12:36:27 +00:00
a1346054
ef244a5490
Fix typos 2021-09-22 12:36:26 +00:00
Brian May
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
dependabot[bot]
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
Brian May
83038f0472
Merge pull request #676 from balping/master
add openSUSE install instructions
2021-09-07 08:57:25 +10:00
Balázs Dura-Kovács
9974ca17f8
add openSUSE install instructions 2021-09-06 23:47:12 +01:00
Brian May
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
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
Brian May
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
Scott Kuhl
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
Scott Kuhl
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
Brian May
bd00a530dd
Merge pull request #661 from skuhl/no-ttl
Remove ttl hack & require -r option.
2021-07-15 11:47:57 +10:00
Scott Kuhl
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
Brian May
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
Scott Kuhl
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
Brian May
bea5e1a1f4
Merge pull request #657 from kylekyle/master
Remove psutil from requirements.txt
2021-06-29 08:21:04 +10:00
Kyle King
99787c1459
Remove psutil from requirements.txt 2021-06-26 13:11:24 -04:00
Brian May
ee02e1aaa2
Merge pull request #656 from kylekyle/master
replace psutil with os
2021-06-25 15:06:39 +10:00
Kyle
c06581cf83 replace psutil with os 2021-06-23 21:53:03 -04:00
Brian May
78b80709f2
Merge pull request #650 from skuhl/hw-improve
Improve hostwatch robustness and documentation
2021-06-03 07:28:12 +10:00
Scott Kuhl
d3f4889f21 fix lint errors 2021-06-02 15:32:04 -04:00
Scott Kuhl
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
Brian May
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
dependabot[bot]
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
Brian May
a3cbf0885f
Merge pull request #646 from skuhl/nat-ipv6
Add IPv6 support to nat (iptables) method.
2021-06-01 07:40:08 +10:00
Scott Kuhl
3f201095ea Merge branch 'master' into nat-ipv6 2021-05-30 21:09:32 -04:00
Brian May
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
Brian May
1820264dd5
Merge pull request #647 from skuhl/man-page-updates
Update documentation
2021-05-31 07:48:56 +10:00
Brian May
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
Brian May
d2836a082f
Merge pull request #644 from skuhl/fix-tmark
Fix --tmark option
2021-05-31 07:42:30 +10:00
Scott Kuhl
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
Scott Kuhl
843a729d64 flake8: fix long line 2021-05-29 21:58:56 -04:00
Scott Kuhl
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
Scott Kuhl
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
Scott Kuhl
1dbec7252d Remove dead code in hostwatch.py
11 years ago in commit 384d0e7c1d637c4c36eb3e4d31d538bc9420d987,
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
Scott Kuhl
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
Brian May
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
Scott Kuhl
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
Brian May
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
Brian May
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
dependabot[bot]
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
Brian May
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
Brian May
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
dependabot[bot]
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
dependabot[bot]
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
Brian May
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
dependabot[bot]
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
Brian May
88154f4f9c
Merge pull request #635 from skuhl/sudoers-warning
Warn about adding sshuttle to sudoers.
2021-05-09 20:25:26 +10:00
Scott Kuhl
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
Brian May
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
Scott Kuhl
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
dependabot[bot]
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
Brian May
f6176d3581
Merge pull request #630 from thesamesam/patch-1
README.rst: fix Gentoo entry syntax
2021-04-27 07:47:46 +10:00
Sam James
bc08198cae
README.rst: fix Gentoo entry syntax 2021-04-26 16:22:42 +01:00
Brian May
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
dependabot[bot]
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
Brian May
7307636d35
Merge pull request #624 from sshuttle/dependabot/add-v2-config-file
Create Dependabot config file
2021-04-08 11:47:33 +10:00
dependabot-preview[bot]
1f5161e48c
Create Dependabot config file 2021-04-08 01:40:51 +00:00
Brian May
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
dependabot-preview[bot]
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
Brian May
51a1078371
Merge pull request #622 from necrose99/patch-1
Update README.rst
2021-03-26 07:52:21 +11:00
Brian May
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
Michael L
e54747bfb0
Update README.rst
https://packages.gentoo.org/packages/net-proxy/sshuttle
2021-03-25 14:12:32 -05:00
dependabot-preview[bot]
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
Brian May
bafc0a7038
Merge pull request #618 from patrislav1/bugfix-dnsproxy
Fix DnsProxy
2021-03-21 09:21:26 +11:00
Patrick Huesmann
4c11dd7183 options: make sure ttl is of integer type 2021-03-20 12:26:17 +01:00
Brian May
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
Brian May
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
Patrick Huesmann
5e177d81bc Fix firewall setup/teardown failure on xtable lock 2021-03-18 09:41:21 +01:00
dependabot-preview[bot]
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
Brian May
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
Brian May
97076a9390 Adjust branches, remove temporary branches 2021-03-16 08:29:14 +11:00
Brian May
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
Scott Kuhl
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
dependabot-preview[bot]
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
Victor Kareh
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
Kees Hink
0e51da519f Allow comments in configuration file 2021-02-16 07:51:32 +11:00
Kees Hink
a22c453d5e Remove trailing whitespace 2021-02-16 07:51:32 +11:00
dependabot-preview[bot]
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
Scott Kuhl
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
dependabot-preview[bot]
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
dependabot-preview[bot]
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
masahitojp
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
masahitojp
65d437a9f8 feat: remove mock from test dependencies.
Because mock can be replace by unittest.mock
2021-01-17 15:42:55 +11:00
masahitojp
254b8e7987 remove py35 from tox.ini
Because the supported version is python3.6 and above.
2021-01-17 15:42:24 +11:00
Scott Kuhl
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
Scott Kuhl
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
Scott Kuhl
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
Scott Kuhl
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
Herby Gillot
da848b6dba Readme: add instructions for installing via MacPorts 2020-12-29 19:12:06 +11:00
75 changed files with 5298 additions and 1716 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake .

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

@ -0,0 +1,13 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: uv
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

70
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '31 21 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@ -5,11 +5,10 @@ name: Python package
on:
push:
branches: [ master, tproxy_mark_param ]
branches: [ master ]
pull_request:
branches: [ master, tproxy_mark_param ]
workflow_dispatch:
branches: [ tproxy_mark_param ]
branches: [ master ]
workflow_dispatch: {}
jobs:
build:
@ -17,21 +16,23 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: ["3.9", "3.10", "3.11", "3.12"]
poetry-version: ["main"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-tests.txt
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.4.30"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install the project
run: uv sync --all-extras --dev
- name: Lint with flake8
run: |
flake8 sshuttle tests --count --show-source --statistics
- name: Test with pytest
run: |
PYTHONPATH=$PWD pytest
run: uv run flake8 sshuttle tests --count --show-source --statistics
- name: Run the automated tests
run: uv run pytest -v

66
.github/workflows/release-please.yml vendored Normal file
View File

@ -0,0 +1,66 @@
on:
push:
branches:
- master
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
release-type: python
build-pypi:
name: Build for pypi
needs: [release-please]
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.4.30"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Build project
run: uv build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
upload-pypi:
name: Upload to pypi
needs: [build-pypi]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/sshuttle
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

6
.gitignore vendored
View File

@ -1,5 +1,5 @@
/sshuttle/version.py
/tmp/
/.coverage
/.cache/
/.eggs/
/.tox/
@ -15,4 +15,6 @@
/.redo
/.pytest_cache/
/.python-version
.vscode/
/.direnv/
/result
/.vscode/

13
.readthedocs.yaml Normal file
View File

@ -0,0 +1,13 @@
version: 2
build:
os: ubuntu-20.04
tools:
python: "3.10"
jobs:
post_install:
- pip install uv
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs --link-mode=copy
sphinx:
configuration: docs/conf.py

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
python 3.10.6

54
CHANGELOG.md Normal file
View File

@ -0,0 +1,54 @@
# Changelog
## [1.3.1](https://github.com/sshuttle/sshuttle/compare/v1.3.0...v1.3.1) (2025-03-25)
### Bug Fixes
* add pycodestyle config ([5942376](https://github.com/sshuttle/sshuttle/commit/5942376090395d0a8dfe38fe012a519268199341))
* add python lint tools ([ae3c022](https://github.com/sshuttle/sshuttle/commit/ae3c022d1d67de92f1c4712d06eb8ae76c970624))
* correct bad version number at runtime ([7b66253](https://github.com/sshuttle/sshuttle/commit/7b662536ba92d724ed8f86a32a21282fea66047c))
* Restore "nft" method ([375810a](https://github.com/sshuttle/sshuttle/commit/375810a9a8910a51db22c9fe4c0658c39b16c9e7))
## [1.3.0](https://github.com/sshuttle/sshuttle/compare/v1.2.0...v1.3.0) (2025-02-23)
### Features
* switch to a network namespace on Linux ([8a123d9](https://github.com/sshuttle/sshuttle/commit/8a123d9762b84f168a8ca8c75f73e590954e122d))
### Bug Fixes
* prevent UnicodeDecodeError parsing iptables rule with comments ([cbe3d1e](https://github.com/sshuttle/sshuttle/commit/cbe3d1e402cac9d3fbc818fe0cb8a87be2e94348))
* remove temp build hack ([1f5e6ce](https://github.com/sshuttle/sshuttle/commit/1f5e6cea703db33761fb1c3f999b9624cf3bc7ad))
* support ':' sign in password ([7fa927e](https://github.com/sshuttle/sshuttle/commit/7fa927ef8ceea6b1b2848ca433b8b3e3b63f0509))
### Documentation
* replace nix-env with nix-shell ([340ccc7](https://github.com/sshuttle/sshuttle/commit/340ccc705ebd9499f14f799fcef0b5d2a8055fb4))
* update installation instructions ([a2d405a](https://github.com/sshuttle/sshuttle/commit/a2d405a6a7f9d1a301311a109f8411f2fe8deb37))
## [1.2.0](https://github.com/sshuttle/sshuttle/compare/v1.1.2...v1.2.0) (2025-02-07)
### Features
* Add release-please to build workflow ([d910b64](https://github.com/sshuttle/sshuttle/commit/d910b64be77fd7ef2a5f169b780bfda95e67318d))
### Bug Fixes
* Add support for Python 3.11 and Python 3.11 ([a3396a4](https://github.com/sshuttle/sshuttle/commit/a3396a443df14d3bafc3d25909d9221aa182b8fc))
* bad file descriptor error in windows, fix pytest errors ([d4d0fa9](https://github.com/sshuttle/sshuttle/commit/d4d0fa945d50606360aa7c5f026a0f190b026c68))
* drop Python 3.8 support ([1084c0f](https://github.com/sshuttle/sshuttle/commit/1084c0f2458c1595b00963b3bd54bd667e4cfc9f))
* ensure poetry works for Python 3.9 ([693ee40](https://github.com/sshuttle/sshuttle/commit/693ee40c485c70f353326eb0e8f721f984850f5c))
* fix broken workflow_dispatch CI rule ([4b6f7c6](https://github.com/sshuttle/sshuttle/commit/4b6f7c6a656a752552295863092d3b8af0b42b31))
* Remove more references to legacy Python versions ([339b522](https://github.com/sshuttle/sshuttle/commit/339b5221bc33254329f79f2374f6114be6f30aed))
* replace requirements.txt files with poetry ([85dc319](https://github.com/sshuttle/sshuttle/commit/85dc3199a332f9f9f0e4c6037c883a8f88dc09ca))
* replace requirements.txt files with poetry (2) ([d08f78a](https://github.com/sshuttle/sshuttle/commit/d08f78a2d9777951d7e18f6eaebbcdd279d7683a))
* replace requirements.txt files with poetry (3) ([62da705](https://github.com/sshuttle/sshuttle/commit/62da70510e8a1f93e8b38870fdebdbace965cd8e))
* replace requirements.txt files with poetry (4) ([9bcedf1](https://github.com/sshuttle/sshuttle/commit/9bcedf19049e5b3a8ae26818299cc518ec03a926))
* update nix flake to fix problems ([cda60a5](https://github.com/sshuttle/sshuttle/commit/cda60a52331c7102cff892b9b77c8321e276680a))
* use Python &gt;= 3.10 for docs ([bf29464](https://github.com/sshuttle/sshuttle/commit/bf294643e283cef9fb285d44e307e958686caf46))

View File

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

View File

@ -4,7 +4,7 @@ sshuttle: where transparent proxy meets VPN meets ssh
As far as I know, sshuttle is the only program that solves the following
common case:
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
- Your client machine (or router) is Linux, FreeBSD, MacOS or Windows.
- You have access to a remote network via ssh.
@ -24,73 +24,15 @@ common case:
- You can't use openssh's PermitTunnel feature because
it's disabled by default on openssh servers; plus it does
TCP-over-TCP, which has `terrible performance`_.
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
Obtaining sshuttle
------------------
- Ubuntu 16.04 or later::
apt-get install sshuttle
- Debian stretch or later::
apt-get install sshuttle
- Arch Linux::
pacman -S sshuttle
- Fedora::
dnf install sshuttle
- NixOS::
nix-env -iA nixos.sshuttle
- From PyPI::
sudo pip install sshuttle
- Clone::
git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle
sudo ./setup.py install
- FreeBSD::
# ports
cd /usr/ports/net/py-sshuttle && make install clean
# pkg
pkg install py36-sshuttle
It is also possible to install into a virtualenv as a non-root user.
- From PyPI::
virtualenv -p python3 /tmp/sshuttle
. /tmp/sshuttle/bin/activate
pip install sshuttle
- Clone::
virtualenv -p python3 /tmp/sshuttle
. /tmp/sshuttle/bin/activate
git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle
./setup.py install
- Homebrew::
brew install sshuttle
- Nix::
nix-env -iA nixpkgs.sshuttle
Please see the documentation_.
.. _Documentation: https://sshuttle.readthedocs.io/en/stable/installation.html
Documentation
-------------
@ -103,5 +45,5 @@ https://sshuttle.readthedocs.org/en/latest/
Running as a service
--------------------
Sshuttle can also be run as a service and configured using a config management system:
Sshuttle can also be run as a service and configured using a config management system:
https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe

View File

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

View File

@ -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.
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide

View File

@ -16,7 +16,7 @@
import sys
import os
sys.path.insert(0, os.path.abspath('..'))
import sshuttle.version # NOQA
import sshuttle # NOQA
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@ -56,7 +56,7 @@ copyright = '2016, Brian May'
# built documents.
#
# The full version, including alpha/beta/rc tags.
release = sshuttle.version.version
release = sshuttle.__version__
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
@ -103,7 +103,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = 'furo'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the

View File

@ -34,4 +34,3 @@ sshuttle assembles the TCP stream locally, multiplexes it statefully over
an ssh session, and disassembles it back into packets at the other end. So
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
safe.

View File

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

View File

@ -1,24 +1,84 @@
Installation
============
- Ubuntu 16.04 or later::
apt-get install sshuttle
- Debian stretch or later::
apt-get install sshuttle
- Arch Linux::
pacman -S sshuttle
- Fedora::
dnf install sshuttle
- openSUSE::
zypper in sshuttle
- Gentoo::
emerge -av net-proxy/sshuttle
- NixOS::
nix-env -iA nixos.sshuttle
- From PyPI::
pip install sshuttle
- Debain package manager::
sudo apt install sshuttle
sudo pip install sshuttle
- Clone::
git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle
./setup.py install
sudo ./setup.py install
- FreeBSD::
Optionally after installation
-----------------------------
# ports
cd /usr/ports/net/py-sshuttle && make install clean
# pkg
pkg install py39-sshuttle
- Add to sudoers file::
- OpenBSD::
sshuttle --sudoers
pkg_add sshuttle
- macOS, via MacPorts::
sudo port selfupdate
sudo port install sshuttle
It is also possible to install into a virtualenv as a non-root user.
- From PyPI::
python3 -m venv /tmp/sshuttle
. /tmp/sshuttle/bin/activate
pip install sshuttle
- Clone::
git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle
python3 -m venv /tmp/sshuttle
. /tmp/sshuttle/bin/activate
python -m pip install .
- Homebrew::
brew install sshuttle
- Nix::
nix-shell -p sshuttle
- Windows::
pip install sshuttle

View File

@ -4,14 +4,14 @@ sshuttle
Synopsis
--------
**sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\>
**sshuttle** [*options*] **-r** *[username@]sshserver[:port]* \<*subnets* ...\>
Description
-----------
:program:`sshuttle` allows you to create a VPN connection from your
machine to any remote server that you can connect to via
ssh, as long as that server has python 3.6 or higher.
machine to any remote server that you can connect to via ssh, as long
as that server has a sufficiently new Python installation.
To work, you must have root access on the local machine,
but you can have a normal account on the server.
@ -31,22 +31,23 @@ Options
.. option:: <subnets>
A list of subnets to route over the VPN, in the form
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
netmask), and 0/0 ('just route everything through the
VPN'). Any of the previous examples are also valid if you append
a port or a port range, so 1.2.3.4:8000 will only tunnel traffic
that has as the destination port 8000 of 1.2.3.4 and
1.2.3.0/24:8000-9000 will tunnel traffic going to any port between
8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet.
A hostname can be provided instead of an IP address. If the
hostname resolves to multiple IPs, all of the IPs are included.
If a width is provided with a hostname that the width is applied
to all of the hostnames IPs (if they are all either IPv4 or IPv6).
Widths cannot be supplied to hostnames that resolve to both IPv4
and IPv6. Valid examples are example.com, example.com:8000,
example.com/24, example.com/24:8000 and example.com:8000-9000.
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
single IP address) and 1.2.3.4/32 (equivalent to 1.2.3.4),
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 netmask).
Specify subnets 0/0 to match all IPv4 addresses and ::/0 to match
all IPv6 addresses. Any of the previous examples are also valid if
you append a port or a port range, so 1.2.3.4:8000 will only
tunnel traffic that has as the destination port 8000 of 1.2.3.4
and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port
between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24
subnet. A hostname can be provided instead of an IP address. If
the hostname resolves to multiple IPs, all of the IPs are
included. If a width is provided with a hostname, the width is
applied to all of the hostnames IPs (if they are all either IPv4
or IPv6). Widths cannot be supplied to hostnames that resolve to
both IPv4 and IPv6. Valid examples are example.com,
example.com:8000, example.com/24, example.com/24:8000 and
example.com:8000-9000.
.. option:: --method <auto|nat|nft|tproxy|pf|ipfw>
@ -68,8 +69,8 @@ Options
You can use any name resolving to an IP address of the machine running
:program:`sshuttle`, e.g. ``--listen localhost``.
For the nft, tproxy and pf methods this can be an IPv6 address. Use
this option with comma separated values if required, to provide both
For the nft, tproxy and pf methods this can be an IPv6 address. Use
this option with comma separated values if required, to provide both
IPv4 and IPv6 addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``.
.. option:: -H, --auto-hosts
@ -88,6 +89,13 @@ Options
few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else.
:program:`sshuttle` tries to store a cache of the hostnames in
~/.sshuttle.hosts on the remote host. Similarly, it tries to read
the file when you later reconnect to the host with --auto-hosts
enabled to quickly populate the host list. When troubleshooting
this feature, try removing this file on the remote host when
sshuttle is not running.
.. option:: -N, --auto-nets
In addition to the subnets provided on the command
@ -104,7 +112,7 @@ Options
Capture local DNS requests and forward to the remote DNS
server. All queries to any of the local system's DNS
servers (/etc/resolv.conf and, if it exists,
servers (/etc/resolv.conf and, if it exists,
/run/systemd/resolve/resolv.conf) will be intercepted and
resolved on the remote side of the tunnel instead, there
using the DNS specified via the :option:`--to-ns` option,
@ -141,7 +149,10 @@ Options
The remote hostname and optional username and ssh
port number to use for connecting to the remote server.
For example, example.com, testuser@example.com,
testuser@example.com:2222, or example.com:2244.
testuser@example.com:2222, or example.com:2244. This
hostname is passed to ssh, so it will recognize any
aliases and settings you may have configured in
~/.ssh/config.
.. option:: -x <subnet>, --exclude=<subnet>
@ -170,11 +181,23 @@ Options
in a non-standard location or you want to provide extra
options to the ssh command, for example, ``-e 'ssh -v'``.
.. option:: --remote-shell
For Windows targets, specify configured remote shell program alternative to defacto posix shell.
It would be either ``cmd`` or ``powershell`` unless something like git-bash is in use.
.. option:: --no-cmd-delimiter
Do not add a double dash (--) delimiter before invoking Python on
the remote host. This option is useful when the ssh command used
to connect is a custom command that does not interpret this
delimiter correctly.
.. option:: --seed-hosts
A comma-separated list of hostnames to use to
initialize the :option:`--auto-hosts` scan algorithm.
:option:`--auto-hosts` does things like poll local SMB servers
:option:`--auto-hosts` does things like poll netstat output
for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start
from.
@ -231,8 +254,8 @@ Options
.. option:: --disable-ipv6
Disable IPv6 support for methods that support it (nft, tproxy, and
pf).
Disable IPv6 support for methods that support it (nat, nft,
tproxy, and pf).
.. option:: --firewall
@ -251,32 +274,28 @@ Options
makes it a lot easier to debug and test the :option:`--auto-hosts`
feature.
.. option:: --sudoers
sshuttle will auto generate the proper sudoers.d config file and add it.
Once this is completed, sshuttle will exit and tell the user if
it succeed or not. Do not call this options with sudo, it may generate a
incorrect config file.
.. option:: --sudoers-no-modify
sshuttle will auto generate the proper sudoers.d config and print it to
stdout. The option will not modify the system at all.
sshuttle prints a configuration to stdout which allows a user to
run sshuttle without a password. This option is INSECURE because,
with some cleverness, it also allows the user to run any command
as root without a password. The output also includes a suggested
method for you to install the configuration.
Use --sudoers-user to modify the user that it applies to.
.. option:: --sudoers-user
Set the user name or group with %group_name for passwordless operation.
Default is the current user.set ALL for all users. Only works with
--sudoers or --sudoers-no-modify option.
Set the user name or group with %group_name for passwordless
operation. Default is the current user. Set to ALL for all users
(NOT RECOMMENDED: See note about security in --sudoers-no-modify
documentation above). Only works with the --sudoers-no-modify
option.
.. option:: --sudoers-filename
.. option:: -t <mark>, --tmark=<mark>
Set the file name for the sudoers.d file to be added. Default is
"sshuttle_auto". Only works with --sudoers.
.. option:: -t, --tmark
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
@ -305,54 +324,119 @@ Arguments read from a file must be one per line, as shown below::
--option2
value2
The configuration file supports comments for human-readable
annotations. For example::
# company-internal API
8.8.8.8/32
# home IoT
192.168.63.0/24
Environment Variable
--------------------
You can specify command line options with the `SSHUTTLE_ARGS` environment
variable. If a given option is defined in both the environment variable and
command line, the value on the command line will take precedence.
For example::
SSHUTTLE_ARGS="-e 'ssh -v' --dns" sshuttle -r example.com 0/0
Examples
--------
Test locally by proxying all local connections, without using ssh::
$ sshuttle -v 0/0
Use the following command to route all IPv4 TCP traffic through remote
(-r) host example.com (and possibly other traffic too, depending on
the selected --method). The 0/0 subnet, short for 0.0.0.0/0, matches
all IPv4 addresses. The ::/0 subnet, matching all IPv6 addresses could
be added to the example. We also exclude (-x) example.com:22 so that
we can establish ssh connections from our local machine to the remote
host without them being routed through sshuttle. Excluding the remote
host may be necessary on some machines for sshuttle to work properly.
Press Ctrl+C to exit. To also route DNS queries through sshuttle, try
adding --dns. Add or remove -v options to see more or less
information::
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
$ sshuttle -r example.com -x example.com:22 0/0
Starting sshuttle proxy (version ...).
[local sudo] Password:
firewall manager ready.
c : connecting to server...
s: available routes:
s: 192.168.42.0/24
c : connected.
firewall manager: starting transproxy.
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
...etc...
fw: Starting firewall with Python version 3.9.5
fw: ready method name nat.
c : IPv6 disabled since it isn't supported by method nat.
c : Method: nat
c : IPv4: on
c : IPv6: off (not available with nat method)
c : UDP : off (not available with nat method)
c : DNS : off (available)
c : User: off (available)
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
c : (<AddressFamily.AF_INET: 2>, '0.0.0.0', 0, 0, 0)
c : Subnets to exclude from forwarding:
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
c : TCP redirector listening on ('127.0.0.1', 12299).
c : Starting client with Python version 3.9.5
c : Connecting to server...
user@example.com's password:
s: Starting server with Python version 3.6.8
s: latency control setting = True
s: auto-nets:False
c : Connected to server.
fw: setting up.
fw: iptables -w -t nat -N sshuttle-12299
fw: iptables -w -t nat -F sshuttle-12299
...
Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
^C
firewall manager: undoing changes.
KeyboardInterrupt
c : Keyboard interrupt: exiting.
c : SW#8:192.168.42.121:47523: deleting
c : SW#6:192.168.42.106:50035: deleting
c : SW'unknown':Mux#1: deleting (1 remain)
c : SW#7:192.168.42.121:60554: deleting (0 remain)
Test connection to a remote server, with automatic hostname
Connect to a remote server, with automatic hostname
and subnet guessing::
$ sshuttle -vNHr example.org
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
firewall manager ready.
c : connecting to server...
$ sshuttle -vNHr example.com -x example.com:22
Starting sshuttle proxy (version ...).
[local sudo] Password:
fw: Starting firewall with Python version 3.9.5
fw: ready method name nat.
c : IPv6 disabled since it isn't supported by method nat.
c : Method: nat
c : IPv4: on
c : IPv6: off (not available with nat method)
c : UDP : off (not available with nat method)
c : DNS : off (available)
c : User: off (available)
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
c : NOTE: Additional subnets to forward may be added below by --auto-nets.
c : Subnets to exclude from forwarding:
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
c : TCP redirector listening on ('127.0.0.1', 12300).
c : Starting client with Python version 3.9.5
c : Connecting to server...
user@example.com's password:
s: Starting server with Python version 3.6.8
s: latency control setting = True
s: auto-nets:True
c : Connected to server.
c : seed_hosts: []
s: available routes:
s: 77.141.99.0/24
c : connected.
c : seed_hosts: []
firewall manager: starting transproxy.
hostwatch: Found: testbox1: 1.2.3.4
hostwatch: Found: mytest2: 5.6.7.8
hostwatch: Found: domaincontroller: 99.1.2.3
fw: setting up.
fw: iptables -w -t nat -N sshuttle-12300
fw: iptables -w -t nat -F sshuttle-12300
...
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
^C
firewall manager: undoing changes.
c : Keyboard interrupt: exiting.
c : SW#6:192.168.42.121:60554: deleting
c : SW'unknown':Mux#1: deleting (1 remain)
c : SW#7:192.168.42.121:60554: deleting (0 remain)
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
@ -376,9 +460,7 @@ Example configuration file::
Discussion
----------
When it starts, :program:`sshuttle` creates an ssh session to the
server specified by the ``-r`` option. If ``-r`` is omitted,
it will start both its client and server locally, which is
sometimes useful for testing.
server specified by the ``-r`` option.
After connecting to the remote server, :program:`sshuttle` uploads its
(python) source code to the remote end and executes it
@ -397,7 +479,7 @@ Packet-level forwarding (eg. using the tun/tap devices on
Linux) seems elegant at first, but it results in
several problems, notably the 'tcp over tcp' problem. The
tcp protocol depends fundamentally on packets being dropped
in order to implement its congestion control agorithm; if
in order to implement its congestion control algorithm; if
you pass tcp packets through a tcp-based tunnel (such as
ssh), the inner tcp packets will never be dropped, and so
the inner tcp stream's congestion control will be

View File

@ -6,7 +6,7 @@ Client side Requirements
- sudo, or root access on your client machine.
(The server doesn't need admin access.)
- Python 3.6 or greater.
- Python 3.9 or greater.
Linux with NAT method
@ -15,10 +15,12 @@ Supports:
* IPv4 TCP
* IPv4 DNS
* IPv6 TCP
* IPv6 DNS
Requires:
* iptables DNAT, REDIRECT, and ttl modules.
* iptables DNAT and REDIRECT modules. ip6tables for IPv6.
Linux with nft method
~~~~~~~~~~~~~~~~~~~~~
@ -38,11 +40,11 @@ Linux with TPROXY method
Supports:
* IPv4 TCP
* IPv4 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
* IPv4 UDP
* IPv4 DNS
* IPv6 TCP
* IPv6 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recvmsg`` - see below)
* IPv6 UDP
* IPv6 DNS
MacOS / FreeBSD / OpenBSD / pfSense
@ -63,14 +65,13 @@ Requires:
Windows
~~~~~~~
Not officially supported, however can be made to work with Vagrant. Requires
cmd.exe with Administrator access. See :doc:`windows` for more information.
Experimental built-in support available. See :doc:`windows` for more information.
Server side Requirements
------------------------
- Python 3.6 or greater.
- Python 3.9 or greater.
Additional Suggested Software
@ -79,7 +80,7 @@ Additional Suggested Software
- If you are using systemd, sshuttle can notify it when the connection to
the remote end is established and the firewall rules are installed. For
this feature to work you must configure the process start-up type for the
sshuttle service unit to notify, as shown in the example below.
sshuttle service unit to notify, as shown in the example below.
.. code-block:: ini
:emphasize-lines: 6
@ -87,10 +88,10 @@ Additional Suggested Software
[Unit]
Description=sshuttle
After=network.target
[Service]
Type=notify
ExecStart=/usr/bin/sshuttle --dns --remote <user>@<server> <subnets...>
[Install]
WantedBy=multi-user.target

View File

@ -1,6 +1,6 @@
TPROXY
======
TPROXY is the only method that has full support of IPv6 and UDP.
TPROXY is the only method that supports UDP.
There are some things you need to consider for TPROXY to work:
@ -11,25 +11,21 @@ There are some things you need to consider for TPROXY to work:
ip rule add fwmark {TMARK} lookup 100
ip -6 route add local default dev lo table 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
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
- The client needs to be run as root. e.g.::
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
- You may need to exclude the IP address of the server you are connecting to.
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
work. Use the ``--exclude`` parameter for this.
- Similarly, UDP return packets (including DNS) could get intercepted and
bounced back. This is the case if you have a broad subnet such as
``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the
``--exclude`` parameter for this.
- You need the ``--method=tproxy`` parameter, as above.
- The routes for the outgoing packets must already exist. For example, if your

View File

@ -33,4 +33,3 @@ That project I did for Slipstream was what first gave me the idea to merge
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
program that was the best of all worlds. And here we are, at last.
You're welcome.

View File

@ -11,6 +11,10 @@ Forward all traffic::
sshuttle -r username@sshserver 0.0.0.0/0
- Use the :option:`sshuttle -r` parameter to specify a remote server.
On some systems, you may also need to use the :option:`sshuttle -x`
parameter to exclude sshserver or sshserver:22 so that your local
machine can communicate directly to sshserver without it being
redirected by sshuttle.
- By default sshuttle will automatically choose a method to use. Override with
the :option:`sshuttle --method` parameter.
@ -47,7 +51,7 @@ were right there. And if your "client" machine is a router, everyone on
your local network can make connections to your remote network.
You don't need to install sshuttle on the remote server;
the remote server just needs to have python available.
the remote server just needs to have python available.
sshuttle will automatically upload and run its source code
to the remote python interpreter.
@ -67,44 +71,23 @@ admin access on the server.
Sudoers File
------------
sshuttle can auto-generate the proper sudoers.d file using the current user
for Linux and OSX. Doing this will allow sshuttle to run without asking for
the local sudo password and to give users who do not have sudo access
ability to run sshuttle::
sshuttle --sudoers
sshuttle can generate a sudoers.d file for Linux and MacOS. This
allows one or more users to run sshuttle without entering the
local sudo password. **WARNING:** This option is *insecure*
because, with some cleverness, it also allows these users to run any
command (via the --ssh-cmd option) as root without a password.
DO NOT run this command with sudo, it will ask for your sudo password when
it is needed.
A costume user or group can be set with the :
option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid
values for this vary based on how your system is configured. Values such as
usernames, groups pre-pended with `%` and sudoers user aliases will work. See
the sudoers manual for more information on valid user specif actions.
The options must be used with `--sudoers`::
sshuttle --sudoers --sudoers-user mike
sshuttle --sudoers --sudoers-user %sudo
The name of the file to be added to sudoers.d can be configured as well. This
is mostly not necessary but can be useful for giving more than one user
access to sshuttle. The default is `sshuttle_auto`::
sshuttle --sudoer --sudoers-filename sshuttle_auto_mike
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
You can also see what configuration will be added to your system without
modifying anything. This can be helpfull is the auto feature does not work, or
you want more control. This option also works with `--sudoers-username`.
`--sudoers-filename` has no effect with this option::
To print a sudo configuration file and see a suggested way to install it, run::
sshuttle --sudoers-no-modify
This will simply sprint the generated configuration to STDOUT. Example::
A custom user or group can be set with the
:option:`sshuttle --sudoers-no-modify --sudoers-user {user_descriptor}`
option. Valid values for this vary based on how your system is configured.
Values such as usernames, groups prepended with `%` and sudoers user
aliases will work. See the sudoers manual for more information on valid
user-specified actions. The option must be used with `--sudoers-no-modify`::
08:40 PM william$ sshuttle --sudoers-no-modify
Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall
william ALL=NOPASSWD: SSHUTTLE304
sshuttle --sudoers-no-modify --sudoers-user mike
sshuttle --sudoers-no-modify --sudoers-user %sudo

View File

@ -1,7 +1,16 @@
Microsoft Windows
=================
Currently there is no built in support for running sshuttle directly on
Microsoft Windows.
Experimental native support::
Experimental built-in support for Windows is available through `windivert` method.
You have to install https://pypi.org/project/pydivert package. You need Administrator privileges to use windivert method
Notes
- sshuttle should be executed from admin shell (Automatic firewall process admin elevation is not available)
- TCP/IPv4 supported (IPv6/UDP/DNS are not available)
Use Linux VM on Windows::
What we can really do is to create a Linux VM with Vagrant (or simply
Virtualbox if you like). In the Vagrant settings, remember to turn on bridged
@ -16,4 +25,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
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

133
flake.lock generated Normal file
View File

@ -0,0 +1,133 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1740743217,
"narHash": "sha256-brsCRzLqimpyhORma84c3W2xPbIidZlIc3JGIuQVSNI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b27ba4eb322d9d2bf2dc9ada9fd59442f50c8d7c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"pyproject-build-systems": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
],
"uv2nix": [
"uv2nix"
]
},
"locked": {
"lastModified": 1740362541,
"narHash": "sha256-S8Mno07MspggOv/xIz5g8hB2b/C5HPiX8E+rXzKY+5U=",
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"rev": "e151741c848ba92331af91f4e47640a1fb82be19",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"type": "github"
}
},
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1739758351,
"narHash": "sha256-Aoa4dEoC7Hf6+gFVk/SDquZTMFlmlfsgdTWuqQxzePs=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "1329712f7f9af3a8b270764ba338a455b7323811",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"pyproject-build-systems": "pyproject-build-systems",
"pyproject-nix": "pyproject-nix",
"uv2nix": "uv2nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"uv2nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
]
},
"locked": {
"lastModified": 1740497536,
"narHash": "sha256-K+8wsVooqhaqyxuvew3+62mgOfRLJ7whv7woqPU3Ypo=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "d01fd3a141755ad5d5b93dd9fcbd76d6401f5bac",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

117
flake.nix Normal file
View File

@ -0,0 +1,117 @@
{
description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling.";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
uv2nix = {
url = "github:pyproject-nix/uv2nix";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
pyproject-build-systems = {
url = "github:pyproject-nix/build-system-pkgs";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.uv2nix.follows = "uv2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
flake-utils,
pyproject-nix,
uv2nix,
pyproject-build-systems,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
inherit (nixpkgs) lib;
pkgs = nixpkgs.legacyPackages.${system};
python = pkgs.python312;
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
# Create package overlay from workspace.
overlay = workspace.mkPyprojectOverlay {
sourcePreference = "sdist";
};
# Extend generated overlay with build fixups
#
# Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds.
# This is an additional overlay implementing build fixups.
# See:
# - https://pyproject-nix.github.io/uv2nix/FAQ.html
pyprojectOverrides =
final: prev:
# Implement build fixups here.
# Note that uv2nix is _not_ using Nixpkgs buildPythonPackage.
# It's using https://pyproject-nix.github.io/pyproject.nix/build.html
let
inherit (final) resolveBuildSystem;
inherit (builtins) mapAttrs;
# Build system dependencies specified in the shape expected by resolveBuildSystem
# The empty lists below are lists of optional dependencies.
#
# A package `foo` with specification written as:
# `setuptools-scm[toml]` in pyproject.toml would be written as
# `foo.setuptools-scm = [ "toml" ]` in Nix
buildSystemOverrides = {
chardet.setuptools = [ ];
colorlog.setuptools = [ ];
python-debian.setuptools = [ ];
pluggy.setuptools = [ ];
pathspec.flit-core = [ ];
packaging.flit-core = [ ];
};
in
mapAttrs (
name: spec:
prev.${name}.overrideAttrs (old: {
nativeBuildInputs = old.nativeBuildInputs ++ resolveBuildSystem spec;
})
) buildSystemOverrides;
pythonSet =
(pkgs.callPackage pyproject-nix.build.packages {
inherit python;
}).overrideScope
(
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
);
inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication;
package = mkApplication {
venv = pythonSet.mkVirtualEnv "sshuttle" workspace.deps.default;
package = pythonSet.sshuttle;
};
in
{
packages = {
sshuttle = package;
default = package;
};
devShells.default = pkgs.mkShell {
packages = [
pkgs.uv
];
};
}
);
}

57
pyproject.toml Normal file
View File

@ -0,0 +1,57 @@
[project]
authors = [
{name = "Brian May", email = "brian@linuxpenguins.xyz"},
]
license = {text = "LGPL-2.1"}
requires-python = "<4.0,>=3.9"
dependencies = []
name = "sshuttle"
version = "1.3.1"
description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling."
readme = "README.rst"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: System :: Networking",
]
[project.scripts]
sshuttle = "sshuttle.cmdline:main"
[dependency-groups]
dev = [
"pytest<9.0.0,>=8.0.1",
"pytest-cov<7.0,>=4.1",
"flake8<8.0.0,>=7.0.0",
"pyflakes<4.0.0,>=3.2.0",
"bump2version<2.0.0,>=1.0.1",
"twine<7,>=5",
"black>=25.1.0",
"jedi-language-server>=0.44.0",
"pylsp-mypy>=0.7.0",
"python-lsp-server>=1.12.2",
"ruff>=0.11.2",
]
docs = [
"sphinx==8.1.3; python_version ~= \"3.10\"",
"furo==2024.8.6",
]
[tool.uv]
default-groups = []
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.sdist]
exclude = [
"/.jj"
]

View File

@ -1,7 +0,0 @@
-r requirements.txt
attrs==20.3.0
pytest==6.2.1
pytest-cov==2.10.1
mock==4.0.3
flake8==3.8.4
pyflakes==2.2.0

View File

@ -1,2 +0,0 @@
setuptools-scm==5.0.1
psutil

4
run
View File

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

39
scripts/Containerfile Normal file
View File

@ -0,0 +1,39 @@
# https://hub.docker.com/r/linuxserver/openssh-server/
ARG BASE_IMAGE=docker.io/linuxserver/openssh-server:version-9.3_p2-r1
FROM ${BASE_IMAGE} as pyenv
# https://github.com/pyenv/pyenv/wiki#suggested-build-environment
RUN apk add --no-cache build-base git libffi-dev openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev
ENV PYENV_ROOT=/pyenv
RUN curl https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
RUN /pyenv/bin/pyenv install 3.10
RUN /pyenv/bin/pyenv install 3.11
RUN /pyenv/bin/pyenv install 3.12
RUN bash -xc 'rm -rf /pyenv/{.git,plugins} /pyenv/versions/*/lib/*/{test,config,config-*linux-gnu}' && \
find /pyenv -type d -name __pycache__ -exec rm -rf {} + && \
find /pyenv -type f -name '*.py[co]' -delete
FROM ${BASE_IMAGE}
RUN apk add --no-cache bash nginx iperf3
# pyenv setup
ENV PYENV_ROOT=/pyenv
ENV PATH=/pyenv/shims:/pyenv/bin:$PATH
COPY --from=pyenv /pyenv /pyenv
# OpenSSH Server variables
ENV PUID=1000
ENV PGID=1000
ENV PASSWORD_ACCESS=true
ENV USER_NAME=test
ENV USER_PASSWORD=test
ENV LOG_STDOUT=true
# suppress linuxserver.io logo printing, chnage sshd config
RUN sed -i '1 a exec &>/dev/null' /etc/s6-overlay/s6-rc.d/init-adduser/run
# https://www.linuxserver.io/blog/2019-09-14-customizing-our-containers
# To customize the container and start other components
COPY container.setup.sh /custom-cont-init.d/setup.sh

21
scripts/README.md Normal file
View File

@ -0,0 +1,21 @@
# Container based test bed for sshuttle
```bash
test-bed up -d # start containers
exec-sshuttle <node-id> [--copy-id] [--server-py=2.7|3.10] [--client-py=2.7|3.10] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...]
# --copy-id -> optionally do ssh-copy-id to make it passwordless for future runs
# --sshuttle-bin -> use another sshuttle binary instead of one from dev setup
# --server-py -> Python version to use in server. (manged by pyenv)
# --client-py -> Python version to use in client (manged by pyenv)
exec-sshuttle node-1 # start sshuttle to connect to node-1
exec-tool curl node-1 # curl to nginx instance running on node1 via IP that is only reachable via sshuttle
exec-tool iperf3 node-1 # measure throughput to node-1
run-benchmark node-1 --client-py=3.10
```
<https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_server_configuration#configuring-the-default-shell-for-openssh-in-windows>

34
scripts/compose.yml Normal file
View File

@ -0,0 +1,34 @@
name: sshuttle-testbed
services:
node-1:
image: ghcr.io/sshuttle/sshuttle-testbed
container_name: sshuttle-testbed-node-1
hostname: node-1
cap_add:
- "NET_ADMIN"
environment:
- ADD_IP_ADDRESSES=10.55.1.77/24
networks:
default:
ipv6_address: 2001:0DB8::551
node-2:
image: ghcr.io/sshuttle/sshuttle-testbed
container_name: sshuttle-testbed-node-2
hostname: node-2
cap_add:
- "NET_ADMIN"
environment:
- ADD_IP_ADDRESSES=10.55.2.77/32
networks:
default:
ipv6_address: 2001:0DB8::552
networks:
default:
driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: 2001:0DB8::/112
# internal: true

65
scripts/container.setup.sh Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
set -e
function with_set_x() {
set -x
"$@"
{
ec=$?
set +x
return $ec
} 2>/dev/null
}
function log() {
echo "$*" >&2
}
log ">>> Setting up $(hostname) | id: $(id)\nIP:\n$(ip a)\nRoutes:\n$(ip r)\npyenv:\n$(pyenv versions)"
echo "
AcceptEnv PYENV_VERSION
" >> /etc/ssh/sshd_config
iface="$(ip route | awk '/default/ { print $5 }')"
default_gw="$(ip route | awk '/default/ { print $3 }')"
for addr in ${ADD_IP_ADDRESSES//,/ }; do
log ">>> Adding $addr to interface $iface"
net_addr=$(ipcalc -n "$addr" | awk -F= '{print $2}')
with_set_x ip addr add "$addr" dev "$iface"
with_set_x ip route add "$net_addr" via "$default_gw" dev "$iface" # so that sshuttle -N can discover routes
done
log ">>> Starting iperf3 server"
iperf3 --server --port 5001 &
mkdir -p /www
echo "<h5>Hello from $(hostname)</h5>
<pre>
<u>ip address</u>
$(ip address)
<u>ip route</u>
$(ip route)
</pre>" >/www/index.html
echo "
daemon off;
worker_processes 1;
error_log /dev/stdout info;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
server {
access_log /dev/stdout;
listen 8080 default_server;
listen [::]:8080 default_server;
root /www;
}
}" >/etc/nginx/nginx.conf
log ">>> Starting nginx"
nginx &

159
scripts/exec-sshuttle Executable file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env bash
set -e
export MSYS_NO_PATHCONV=1
function with_set_x() {
set -x
"$@"
{
ec=$?
set +x
return $ec
} 2>/dev/null
}
function log() {
echo "$*" >&2
}
ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
ssh_copy_id=false
args=()
subnet_args=()
while [[ $# -gt 0 ]]; do
arg=$1
shift
case "$arg" in
-v|-vv*)
ssh_cmd+=" -v"
args+=("$arg")
;;
-r)
args+=("-r" "$1")
shift
;;
--copy-id)
ssh_copy_id=true
;;
--server-py=*)
server_pyenv_ver="${arg#*=}"
;;
--client-py=*)
client_pyenv_ver="${arg#*=}"
;;
-6)
ipv6_only=true
;;
--sshuttle-bin=*)
sshuttle_bin="${arg#*=}"
;;
-N|*/*)
subnet_args+=("$arg")
;;
-*)
args+=("$arg")
;;
*)
if [[ -z "$target" ]]; then
target=$arg
else
args+=("$arg")
fi
;;
esac
done
if [[ ${#subnet_args[@]} -eq 0 ]]; then
subnet_args=("-N")
fi
if [[ $target == node-* ]]; then
log "Target is a a test-bed node"
port="2222"
user_part="test:test"
host=$("$(dirname "$0")/test-bed" get-ip "$target")
index=${target#node-}
if [[ $ipv6_only == true ]]; then
args+=("2001:0DB8::/112")
else
args+=("10.55.$index.0/24")
fi
target="$user_part@$host:$port"
if ! command -v sshpass >/dev/null; then
log "sshpass is not found. You might have to manually enter ssh password: 'test'"
fi
if [[ -z $server_pyenv_ver ]]; then
log "server-py argumwnt is not specified. Setting it to 3.8"
server_pyenv_ver="3.8"
fi
fi
if [[ -n $server_pyenv_ver ]]; then
log "Would pass PYENV_VERRSION=$server_pyenv_ver to server. pyenv is required on server to make it work"
pycmd="/pyenv/shims/python"
ssh_cmd+=" -o SetEnv=PYENV_VERSION=${server_pyenv_ver:-'3'}"
args=("--python=$pycmd" "${args[@]}")
fi
if [[ $ssh_copy_id == true ]]; then
log "Trying to make it passwordless"
if [[ $target == *@* ]]; then
user_part="${target%%@*}"
host_part="${target#*@}"
else
user_part="$(whoami)"
host_part="$target"
fi
if [[ $host_part == *:* ]]; then
host="${host_part%:*}"
port="${host_part#*:}"
else
host="$host_part"
port="22"
fi
if [[ $user_part == *:* ]]; then
user="${user_part%:*}"
password="${user_part#*:}"
else
user="$user_part"
password=""
fi
cmd=(ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host")
if [[ -n $password ]] && command -v sshpass >/dev/null; then
cmd=(sshpass -p "$password" "${cmd[@]}")
fi
with_set_x "${cmd[@]}"
fi
if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then
cd "$(dirname "$0")/.."
export PYTHONPATH="."
if [[ -n $client_pyenv_ver ]]; then
log "Using pyenv version: $client_pyenv_ver"
command -v pyenv &>/dev/null || log "You have to install pyenv to use --client-py" && exit 1
sshuttle_cmd=(/usr/bin/env PYENV_VERSION="$client_pyenv_ver" pyenv exec python -m sshuttle)
else
log "Using best python version availble"
if [ -x "$(command -v python3)" ] &&
python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
sshuttle_cmd=(python3 -m sshuttle)
else
sshuttle_cmd=(python -m sshuttle)
fi
fi
else
[[ -n $client_pyenv_ver ]] && log "Can't specify --client-py when --sshuttle-bin is specified" && exit 1
sshuttle_cmd=("$sshuttle_bin")
fi
if [[ " ${args[*]} " != *" --ssh-cmd "* ]]; then
args=("--ssh-cmd" "$ssh_cmd" "${args[@]}")
fi
if [[ " ${args[*]} " != *" -r "* ]]; then
args=("-r" "$target" "${args[@]}")
fi
set -x
"${sshuttle_cmd[@]}" --version
exec "${sshuttle_cmd[@]}" "${args[@]}" "${subnet_args[@]}"

86
scripts/exec-tool Executable file
View File

@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -e
function with_set_x() {
set -x
"$@"
{
ec=$?
set +x
return $ec
} 2>/dev/null
}
function log() {
echo "$*" >&2
}
args=()
while [[ $# -gt 0 ]]; do
arg=$1
shift
case "$arg" in
-6)
ipv6_only=true
continue
;;
-*) ;;
*)
if [[ -z $tool ]]; then
tool=$arg
continue
elif [[ -z $node ]]; then
node=$arg
continue
fi
;;
esac
args+=("$arg")
done
tool=${tool?:"tool argument missing. should be one of iperf3,ping,curl,ab"}
node=${node?:"node argument missing. should be 'node-1' , 'node-2' etc"}
if [[ $node == node-* ]]; then
index=${node#node-}
if [[ $ipv6_only == true ]]; then
host="2001:0DB8::55$index"
else
host="10.55.$index.77"
fi
else
host=$node
fi
connect_timeout_sec=3
case "$tool" in
ping)
with_set_x exec ping -W $connect_timeout_sec "${args[@]}" "$host"
;;
iperf3)
port=5001
with_set_x exec iperf3 --client "$host" --port=$port --connect-timeout=$((connect_timeout_sec * 1000)) "${args[@]}"
;;
curl)
port=8080
if [[ $host = *:* ]]; then
host="[$host]"
args+=(--ipv6)
fi
with_set_x exec curl "http://$host:$port/" -v --connect-timeout $connect_timeout_sec "${args[@]}"
;;
ab)
port=8080
if [[ " ${args[*]}" != *" -n "* && " ${args[*]}" != *" -c "* ]]; then
args+=(-n 500 -c 50 "${args[@]}")
fi
with_set_x exec ab -s $connect_timeout_sec "${args[@]}" "http://$host:$port/"
;;
*)
log "Unknown tool: $tool"
exit 2
;;
esac

40
scripts/run-benchmark Executable file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
function with_set_x() {
set -x
"$@"
{
ec=$?
set +x
return $ec
} 2>/dev/null
}
function log() {
echo "$*" >&2
}
./test-bed up -d
benchmark() {
log -e "\n======== Benchmarking sshuttle | Args: [$*] ========"
local node=$1
shift
with_set_x ./exec-sshuttle "$node" --listen 55771 "$@" &
sshuttle_pid=$!
trap 'kill -0 $sshuttle_pid &>/dev/null && kill -15 $sshuttle_pid' EXIT
while ! nc -z localhost 55771; do sleep 0.1; done
sleep 1
./exec-tool iperf3 "$node" --time=4
with_set_x kill -15 $sshuttle_pid
wait $sshuttle_pid || true
}
if [[ $# -gt 0 ]]; then
benchmark "${@}"
else
benchmark node-1 --sshuttle-bin="${SSHUTTLE_BIN:-sshuttle}"
benchmark node-1 --sshuttle-bin=dev
fi

9
scripts/run-checks Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
export PYTHONPATH=.
set -x
python -m flake8 sshuttle tests
python -m pytest .

42
scripts/test-bed Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
if [[ -z $1 || $1 = -* ]]; then
set -- up "$@"
fi
function with_set_x() {
set -x
"$@"
{
ec=$?
set +x
return $ec
} 2>/dev/null
}
function build() {
# podman build -t ghcr.io/sshuttle/sshuttle-testbed .
with_set_x docker build --progress=plain -t ghcr.io/sshuttle/sshuttle-testbed -f Containerfile .
}
function compose() {
# podman-compose "$@"
with_set_x docker compose "$@"
}
function get-ip() {
local container_name=sshuttle-testbed-"$1"
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container_name"
}
if [[ $1 == get-ip ]]; then
shift
get-ip "$@"
else
if [[ $* = *--build* ]]; then
build
fi
compose "$@"
fi

View File

@ -1,17 +1,30 @@
[bumpversion]
current_version = 1.3.1
[bumpversion:file:setup.py]
[bumpversion:file:pyproject.toml]
[bumpversion:file:sshuttle/version.py]
[aliases]
test=pytest
test = pytest
[bdist_wheel]
universal = 1
[upload]
sign=true
identity=0x1784577F811F6EAC
sign = true
identity = 0x1784577F811F6EAC
[flake8]
count=true
show-source=true
statistics=true
count = true
show-source = true
statistics = true
max-line-length = 128
[pycodestyle]
max-line-length = 128
[tool:pytest]
addopts = --cov=sshuttle --cov-branch --cov-report=term-missing

View File

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

View File

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

View File

@ -1,4 +1,10 @@
"""Coverage.py's main entry point."""
import sys
import os
from sshuttle.cmdline import main
sys.exit(main())
from sshuttle.helpers import debug3
debug3("Start: (pid=%s, ppid=%s) %r" % (os.getpid(), os.getppid(), sys.argv))
exit_code = main()
debug3("Exit: (pid=%s, ppid=%s, code=%s) cmd %r" % (os.getpid(), os.getppid(), exit_code, sys.argv))
sys.exit(exit_code)

View File

@ -1,22 +1,29 @@
import sys
import zlib
import types
import platform
stdin = stdin # type: typing.BinaryIO # noqa: F821 must be a previously defined global
verbosity = verbosity # type: int # 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()))
verbosity = verbosity # noqa: F821 must be a previously defined global
z = zlib.decompressobj()
while 1:
name = sys.stdin.readline().strip()
name = stdin.readline().strip()
if name:
# python2 compat: in python2 sys.stdin.readline().strip() -> str
# in python3 sys.stdin.readline().strip() -> bytes
# python2 compat: in python2 stdin.readline().strip() -> str
# in python3 stdin.readline().strip() -> bytes
# (see #481)
if sys.version_info >= (3, 0):
name = name.decode("ASCII")
nbytes = int(sys.stdin.readline())
nbytes = int(stdin.readline())
if verbosity >= 2:
sys.stderr.write(' s: assembling %r (%d bytes)\n'
% (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes))
content = z.decompress(stdin.read(nbytes))
module = types.ModuleType(name)
parents = name.rsplit(".", 1)
@ -40,5 +47,7 @@ sshuttle.helpers.verbose = verbosity
import sshuttle.cmdline_options as options # noqa: E402
from sshuttle.server import main # noqa: E402
main(options.latency_control, options.auto_hosts, options.to_nameserver,
main(options.latency_control, options.latency_buffer_size,
options.auto_hosts, options.to_nameserver,
options.auto_nets)

View File

@ -5,8 +5,8 @@ import time
import subprocess as ssubprocess
import os
import sys
import base64
import platform
import psutil
import sshuttle.helpers as helpers
import sshuttle.ssnet as ssnet
@ -15,36 +15,29 @@ import sshuttle.ssyslog as ssyslog
import sshuttle.sdnotify as sdnotify
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
resolvconf_nameservers, which
resolvconf_nameservers, which, is_admin_user, RWPair
from sshuttle.methods import get_method, Features
from sshuttle import __version__
try:
from pwd import getpwnam
except ImportError:
getpwnam = None
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
except ImportError:
import socket
from grp import getgrnam
except ImportError:
getgrnam = None
import socket
_extra_fd = os.open(os.devnull, os.O_RDONLY)
def got_signal(signum, frame):
log('exiting on signal %d\n' % signum)
log('exiting on signal %d' % signum)
sys.exit(1)
# Filename of the pidfile created by the sshuttle client.
_pidname = None
@ -57,7 +50,7 @@ def check_daemon(pidfile):
if e.errno == errno.ENOENT:
return # no pidfile, ok
else:
raise Fatal("c : can't read %s: %s" % (_pidname, e))
raise Fatal("can't read %s: %s" % (_pidname, e))
if not oldpid:
os.unlink(_pidname)
return # invalid pidfile, ok
@ -80,13 +73,25 @@ def check_daemon(pidfile):
def daemonize():
# Try to open the pidfile prior to forking. If there is a problem,
# the client can then exit with a proper exit status code and
# message.
try:
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
except PermissionError:
# User will have to look in syslog for error message since
# --daemon implies --syslog, all output gets redirected to
# syslog.
raise Fatal("failed to create/write pidfile %s" % _pidname)
# Create a daemon process with a new session id.
if os.fork():
os._exit(0)
os.setsid()
if os.fork():
os._exit(0)
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
# Write pid to the pidfile.
try:
os.write(outfd, b'%d\n' % os.getpid())
finally:
@ -123,14 +128,14 @@ class MultiListener:
self.bind_called = False
def setsockopt(self, level, optname, value):
assert(self.bind_called)
assert self.bind_called
if self.v6:
self.v6.setsockopt(level, optname, value)
if self.v4:
self.v4.setsockopt(level, optname, value)
def add_handler(self, handlers, callback, method, mux):
assert(self.bind_called)
assert self.bind_called
socks = []
if self.v6:
socks.append(self.v6)
@ -145,14 +150,14 @@ class MultiListener:
)
def listen(self, backlog):
assert(self.bind_called)
assert self.bind_called
if self.v6:
self.v6.listen(backlog)
if self.v4:
try:
self.v4.listen(backlog)
except socket.error as e:
# on some systems v4 bind will fail if the v6 suceeded,
# on some systems v4 bind will fail if the v6 succeeded,
# in this case the v6 socket will receive v4 too.
if e.errno == errno.EADDRINUSE and self.v6:
self.v4 = None
@ -160,11 +165,26 @@ class MultiListener:
raise e
def bind(self, address_v6, address_v4):
assert(not self.bind_called)
assert not self.bind_called
self.bind_called = True
if address_v6 is not None:
self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto)
self.v6.bind(address_v6)
try:
self.v6.bind(address_v6)
except OSError as e:
if e.errno == errno.EADDRNOTAVAIL:
# On an IPv6 Linux machine, this situation occurs
# if you run the following prior to running
# sshuttle:
#
# echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6
# echo 1 > /proc/sys/net/ipv6/conf/default/disable_ipv6
raise Fatal("Could not bind to an IPv6 socket with "
"address %s and port %s. "
"Potential workaround: Run sshuttle "
"with '--disable-ipv6'."
% (str(address_v6[0]), str(address_v6[1])))
raise e
else:
self.v6 = None
if address_v4 is not None:
@ -174,81 +194,203 @@ class MultiListener:
self.v4 = None
def print_listening(self, what):
assert(self.bind_called)
assert self.bind_called
if self.v6:
listenip = self.v6.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v6))
debug1('%s listening on %r.' % (what, listenip))
debug2('%s listening with %r.' % (what, self.v6))
if self.v4:
listenip = self.v4.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v4))
debug1('%s listening on %r.' % (what, listenip))
debug2('%s listening with %r.' % (what, self.v4))
class FirewallClient:
def __init__(self, method_name, sudo_pythonpath):
self.auto_nets = []
python_path = os.path.dirname(os.path.dirname(__file__))
argvbase = ([sys.executable, sys.argv[0]] +
argv0 = sys.argv[0]
# argv0 is either be a normal Python file or an executable.
# After installed as a package, sshuttle command points to an .exe in Windows and Python shebang script elsewhere.
argvbase = (([sys.executable, sys.argv[0]] if argv0.endswith('.py') else [argv0]) +
['-v'] * (helpers.verbose or 0) +
['--method', method_name] +
['--firewall'])
if ssyslog._p:
argvbase += ['--syslog']
# Determine how to prefix the command in order to elevate privileges.
if platform.platform().startswith('OpenBSD'):
elev_prefix = ['doas'] # OpenBSD uses built in `doas`
# A list of commands that we can try to run to start the firewall.
argv_tries = []
if is_admin_user(): # No need to elevate privileges
argv_tries.append(argvbase)
else:
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
if sys.platform == 'win32':
# runas_path = which("runas")
# if runas_path:
# argv_tries.append([runas_path , '/noprofile', '/user:Administrator', 'python'])
# XXX: Attempt to elevate privilege using 'runas' in windows seems not working.
# Because underlying ShellExecute() Windows api does not allow child process to inherit stdio.
# TODO(nom3ad): Try to implement another way to achieve this.
raise Fatal("Privilege elevation for Windows is not yet implemented. Please run from an administrator shell")
# Look for binary and switch to absolute path if we can find
# it.
path = which(elev_prefix[0])
if path:
elev_prefix[0] = path
# Linux typically uses sudo; OpenBSD uses doas. However, some
# Linux distributions are starting to use doas.
sudo_cmd = ['sudo', '-p', '[local sudo] Password: ']
doas_cmd = ['doas']
if sudo_pythonpath:
elev_prefix += ['/usr/bin/env',
'PYTHONPATH=%s' % python_path]
argv_tries = [elev_prefix + argvbase, argvbase]
# For clarity, try to replace executable name with the
# full path.
doas_path = which("doas")
if doas_path:
doas_cmd[0] = doas_path
sudo_path = which("sudo")
if sudo_path:
sudo_cmd[0] = sudo_path
# 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()
# sudo_pythonpath indicates if we should set the
# PYTHONPATH environment variable when elevating
# privileges. This can be adjusted with the
# --no-sudo-pythonpath option.
if sudo_pythonpath:
pp_prefix = ['/usr/bin/env',
'PYTHONPATH=%s' %
os.path.dirname(os.path.dirname(__file__))]
sudo_cmd = sudo_cmd + pp_prefix
doas_cmd = doas_cmd + pp_prefix
def setup():
# run in the child process
s2.close()
if os.getuid() == 0:
argv_tries = argv_tries[-1:] # last entry only
# Final order should be: sudo/doas command, env
# pythonpath, and then argvbase (sshuttle command).
sudo_cmd = sudo_cmd + argvbase
doas_cmd = doas_cmd + argvbase
# If we can find doas and not sudo or if we are on
# OpenBSD, try using doas first.
if (doas_path and not sudo_path) or platform.platform().startswith('OpenBSD'):
argv_tries = [doas_cmd, sudo_cmd, argvbase]
else:
argv_tries = [sudo_cmd, doas_cmd, argvbase]
# Try all commands in argv_tries in order. If a command
# produces an error, try the next one. If command is
# successful, set 'success' variable and break.
success = False
for argv in argv_tries:
if sys.platform != 'win32':
# 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()
pstdout = s1
pstdin = s1
penv = None
def preexec_fn():
# run in the child process
s2.close()
def get_pfile():
s1.close()
return s2.makefile('rwb')
else:
# In Windows CPython, BSD sockets are not supported as subprocess stdio.
# if client (and firewall) processes is running as admin user, pipe based stdio can be used for communication.
# But if firewall process is spwaned in elevated mode by non-admin client process, access to stdio is lost.
# To work around this, we can use a socketpair.
# But socket need to be "shared" to child process as it can't be directly set as stdio.
can_use_stdio = is_admin_user()
preexec_fn = None
penv = os.environ.copy()
if can_use_stdio:
pstdout = ssubprocess.PIPE
pstdin = ssubprocess.PIPE
def get_pfile():
return RWPair(self.p.stdout, self.p.stdin)
penv['SSHUTTLE_FW_COM_CHANNEL'] = 'stdio'
else:
pstdout = None
pstdin = None
(s1, s2) = socket.socketpair()
socket_share_data = s1.share(self.p.pid)
socket_share_data_b64 = base64.b64encode(socket_share_data)
penv['SSHUTTLE_FW_COM_CHANNEL'] = socket_share_data_b64
def get_pfile():
s1.close()
return s2.makefile('rwb')
try:
if argv[0] == 'su':
sys.stderr.write('[local su] ')
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
debug1("Starting firewall manager with command: %r" % argv)
self.p = ssubprocess.Popen(argv, stdout=pstdout, stdin=pstdin, env=penv,
preexec_fn=preexec_fn)
# No env: Talking to `FirewallClient.start`, which has no i18n.
break
except OSError as e:
log('Spawning firewall manager: %r\n' % argv)
raise Fatal(e)
self.argv = argv
s1.close()
self.pfile = s2.makefile('rwb')
line = self.pfile.readline()
self.check()
if line[0:5] != b'READY':
raise Fatal('%r expected READY, got %r' % (self.argv, line))
method_name = line[6:-1]
self.method = get_method(method_name.decode("ASCII"))
self.method.set_firewall(self)
# This exception will occur if the program isn't
# present or isn't executable.
debug1('Unable to start firewall manager. Popen failed. '
'Command=%r Exception=%s' % (argv, e))
continue
self.argv = argv
self.pfile = get_pfile()
try:
line = self.pfile.readline()
except IOError:
# happens when firewall subprocess exists
line = ''
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
# Normally, READY will be the first text on the first
# line. However, if an administrator replaced sudo with a
# shell script that echos a message to stdout and then
# runs sudo, READY won't be on the first line. To
# workaround this problem, we read a limited number of
# lines until we encounter "READY". Store all of the text
# we skipped in case we need it for an error message.
#
# A proper way to print a sudo warning message is to use
# sudo's lecture feature. sshuttle works correctly without
# this hack if sudo's lecture feature is used instead.
skipped_text = line
for i in range(100):
if line[0:5] == b'READY':
break
line = self.pfile.readline()
skipped_text += line
if line[0:5] != b'READY':
debug1('Unable to start firewall manager. '
'Expected READY, got %r. '
'Command=%r' % (skipped_text, 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 run firewall client process with elevated privileges were failed.")
def setup(self, subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
user, tmark):
user, group, tmark):
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
self.nslist = nslist
@ -258,6 +400,7 @@ class FirewallClient:
self.dnsport_v4 = dnsport_v4
self.udp = udp
self.user = user
self.group = group
self.tmark = tmark
def check(self):
@ -296,8 +439,14 @@ class FirewallClient:
user = bytes(self.user, 'utf-8')
else:
user = b'%d' % self.user
self.pfile.write(b'GO %d %s\n' % (udp, user))
if self.group is None:
group = b'-'
elif isinstance(self.group, str):
group = bytes(self.group, 'utf-8')
else:
group = b'%d' % self.group
self.pfile.write(b'GO %d %s %s %s %d\n' %
(udp, user, group, bytes(self.tmark, 'ascii'), os.getpid()))
self.pfile.flush()
line = self.pfile.readline()
@ -306,8 +455,8 @@ class FirewallClient:
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip):
assert(not re.search(br'[^-\w\.]', hostname))
assert(not re.search(br'[^0-9.]', ip))
assert not re.search(br'[^-\w\.]', hostname)
assert not re.search(br'[^0-9.]', ip)
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
self.pfile.flush()
@ -326,23 +475,23 @@ def expire_connections(now, mux):
remove = []
for chan, timeout in dnsreqs.items():
if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan)
debug3('expiring dnsreqs channel=%d' % chan)
remove.append(chan)
del mux.channels[chan]
for chan in remove:
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
debug3('Remaining DNS requests: %d' % len(dnsreqs))
remove = []
for peer, (chan, timeout) in udp_by_src.items():
if timeout < now:
debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
debug3('expiring UDP channel channel=%d peer=%r' % (chan, peer))
mux.send(chan, ssnet.CMD_UDP_CLOSE, b'')
remove.append(peer)
del mux.channels[chan]
for peer in remove:
del udp_by_src[peer]
debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
debug3('Remaining UDP channels: %d' % len(udp_by_src))
def onaccept_tcp(listener, method, mux, handlers):
@ -351,7 +500,7 @@ def onaccept_tcp(listener, method, mux, handlers):
sock, srcip = listener.accept()
except socket.error as e:
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
debug1('Rejected incoming connection: too many open files!\n')
debug1('Rejected incoming connection: too many open files!')
# free up an fd so we can eat the connection
os.close(_extra_fd)
try:
@ -364,15 +513,15 @@ def onaccept_tcp(listener, method, mux, handlers):
raise
dstip = method.get_tcp_dstip(sock)
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
dstip[0], dstip[1]))
debug1('Accept TCP: %s:%r -> %s:%r.' % (srcip[0], srcip[1],
dstip[0], dstip[1]))
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
debug1("-- ignored: that's my address!\n")
debug1("-- ignored: that's my address!")
sock.close()
return
chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
log('warning: too many open channels. Discarded connection.')
sock.close()
return
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
@ -385,7 +534,7 @@ def onaccept_tcp(listener, method, mux, handlers):
def udp_done(chan, data, method, sock, dstip):
(src, srcport, data) = data.split(b",", 2)
srcip = (src, int(srcport))
debug3('doing send from %r to %r\n' % (srcip, dstip,))
debug3('doing send from %r to %r' % (srcip, dstip,))
method.send_udp(sock, srcip, dstip, data)
@ -395,7 +544,7 @@ def onaccept_udp(listener, method, mux, handlers):
if t is None:
return
srcip, dstip, data = t
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
debug1('Accept UDP: %r -> %r.' % (srcip, dstip,))
if srcip in udp_by_src:
chan, _ = udp_by_src[srcip]
else:
@ -412,7 +561,7 @@ def onaccept_udp(listener, method, mux, handlers):
def dns_done(chan, data, method, sock, srcip, dstip, mux):
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
debug3('dns_done: channel=%d src=%r dst=%r' % (chan, srcip, dstip))
del mux.channels[chan]
del dnsreqs[chan]
method.send_udp(sock, srcip, dstip, data)
@ -427,9 +576,9 @@ def ondns(listener, method, mux, handlers):
# dstip is None if we are using a method where we can't determine
# the destination IP of the DNS request that we captured from the client.
if dstip is None:
debug1('DNS request from %r: %d bytes\n' % (srcip, len(data)))
debug1('DNS request from %r: %d bytes' % (srcip, len(data)))
else:
debug1('DNS request from %r to %r: %d bytes\n' %
debug1('DNS request from %r to %r: %d bytes' %
(srcip, dstip, len(data)))
chan = mux.next_channel()
dnsreqs[chan] = now + 30
@ -440,63 +589,144 @@ def ondns(listener, method, mux, handlers):
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control,
python, latency_control, latency_buffer_size,
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
to_nameserver):
to_nameserver, add_cmd_delimiter, remote_shell):
helpers.logprefix = 'c : '
debug1('Starting client with Python version %s\n'
debug1('Starting client with Python version %s'
% platform.python_version())
method = fw.method
handlers = []
debug1('Connecting to server...\n')
debug1('Connecting to server...')
try:
(serverproc, serversock) = ssh.connect(
(serverproc, rfile, wfile) = ssh.connect(
ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
add_cmd_delimiter=add_cmd_delimiter,
remote_shell=remote_shell,
options=dict(latency_control=latency_control,
latency_buffer_size=latency_buffer_size,
auto_hosts=auto_hosts,
to_nameserver=to_nameserver,
auto_nets=auto_nets))
except socket.error as e:
if e.args[0] == errno.EPIPE:
raise Fatal("c : failed to establish ssh session (1)")
debug3('Error: EPIPE: ' + repr(e))
raise Fatal("failed to establish ssh session (1)")
else:
raise
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
mux = Mux(rfile, wfile)
handlers.append(mux)
expected = b'SSHUTTLE0001'
try:
v = 'x'
while v and v != b'\0':
v = serversock.recv(1)
v = rfile.read(1)
v = 'x'
while v and v != b'\0':
v = serversock.recv(1)
initstring = serversock.recv(len(expected))
v = rfile.read(1)
initstring = rfile.read(len(expected))
except socket.error as e:
if e.args[0] == errno.ECONNRESET:
raise Fatal("c : failed to establish ssh session (2)")
debug3('Error: ECONNRESET ' + repr(e))
raise Fatal("failed to establish ssh session (2)")
else:
raise
# Returns None if process is still running (or returns exit code)
rv = serverproc.poll()
if rv:
raise Fatal('c : server died with error code %d' % rv)
if rv is not None:
errmsg = "server died with error code %d\n" % rv
# Our fatal exceptions return exit code 99
if rv == 99:
errmsg += "This error code likely means that python started and " \
"the sshuttle server started. However, the sshuttle server " \
"may have raised a 'Fatal' exception after it started."
elif rv == 98:
errmsg += "This error code likely means that we were able to " \
"run python on the server, but that the program continued " \
"to the line after we call python's exec() to execute " \
"sshuttle's server code. Try specifying the python " \
"executable to user on the server by passing --python " \
"to sshuttle."
# This error should only be possible when --python is not specified.
elif rv == 97 and not python:
errmsg += "This error code likely means that either we " \
"couldn't find python3 or python in the PATH on the " \
"server or that we do not have permission to run 'exec' in " \
"the /bin/sh shell on the server. Try specifying the " \
"python executable to use on the server by passing " \
"--python to sshuttle."
# POSIX sh standards says error code 127 is used when you try
# to execute a program that does not exist. See section 2.8.2
# of
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08
elif rv == 127:
if python:
errmsg += "This error code likely means that we were not " \
"able to execute the python executable that specified " \
"with --python. You specified '%s'.\n" % python
if python.startswith("/"):
errmsg += "\nTip for users in a restricted shell on the " \
"server: The server may refuse to run programs " \
"specified with an absolute path. Try specifying " \
"just the name of the python executable. However, " \
"if python is not in your PATH and you cannot " \
"run programs specified with an absolute path, " \
"it is possible that sshuttle will not work."
else:
errmsg += "This error code likely means that we were unable " \
"to execute /bin/sh on the remote server. This can " \
"happen if /bin/sh does not exist on the server or if " \
"you are in a restricted shell that does not allow you " \
"to run programs specified with an absolute path. " \
"Try rerunning sshuttle with the --python parameter."
# When the redirected subnet includes the remote ssh host, the
# firewall rules can interrupt the ssh connection to the
# remote machine. This issue impacts some Linux machines. The
# user sees that the server dies with a broken pipe error and
# code 255.
#
# The solution to this problem is to exclude the remote
# server.
#
# There are many github issues from users encountering this
# problem. Most of the discussion on the topic is here:
# https://github.com/sshuttle/sshuttle/issues/191
elif rv == 255:
errmsg += "It might be possible to resolve this error by " \
"excluding the server that you are ssh'ing to. For example, " \
"if you are running 'sshuttle -v -r example.com 0/0' to " \
"redirect all traffic through example.com, then try " \
"'sshuttle -v -r example.com -x example.com 0/0' to " \
"exclude redirecting the connection to example.com itself " \
"(i.e., sshuttle's firewall rules may be breaking the " \
"ssh connection that it previously established). " \
"Alternatively, you may be able to use 'sshuttle -v -r " \
"example.com -x example.com:22 0/0' to redirect " \
"everything except ssh connections between your machine " \
"and example.com."
raise Fatal(errmsg)
if initstring != expected:
raise Fatal('c : expected server init string %r; got %r'
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
log('Connected to server.\n')
log('Connected to server.')
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
log('daemonizing (%s).' % _pidname)
def onroutes(routestr):
if auto_nets:
@ -508,11 +738,11 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
width = int(width)
ip = ip.decode("ASCII")
if family == socket.AF_INET6 and tcp_listener.v6 is None:
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
if family == socket.AF_INET and tcp_listener.v4 is None:
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
else:
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width))
debug2("Adding auto net %d/%s/%d" % (family, ip, width))
fw.auto_nets.append((family, ip, width, 0, 0))
# we definitely want to do this *after* starting ssh, or we might end
@ -532,7 +762,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist)
debug2('got host list: %r' % hostlist)
for line in hostlist.strip().split():
if line:
name, ip = line.split(b',', 1)
@ -548,7 +778,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
dns_listener.add_handler(handlers, ondns, method, mux)
if seed_hosts is not None:
debug1('seed_hosts: %r\n' % seed_hosts)
debug1('seed_hosts: %r' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
def check_ssh_alive():
@ -556,7 +786,9 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
# poll() won't tell us when process exited since the
# process is no longer our child (it returns 0 all the
# 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.' %
serverproc.pid)
else:
@ -574,40 +806,47 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, nslist,
ssh_cmd, remotename, python, latency_control,
latency_buffer_size, dns, nslist,
method_name, seed_hosts, auto_hosts, auto_nets,
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
user, sudo_pythonpath, tmark):
user, group, sudo_pythonpath, add_cmd_delimiter, remote_shell, tmark):
if not remotename:
print("WARNING: You must specify -r/--remote to securely route "
"traffic to a remote machine. Running without -r/--remote "
"is only recommended for testing.")
raise Fatal("You must use -r/--remote to specify a remote "
"host to route traffic through.")
if daemon:
try:
check_daemon(pidfile)
except Fatal as e:
log("%s\n" % e)
log("%s" % e)
return 5
debug1('Starting sshuttle proxy (version %s).\n' % __version__)
debug1('Starting sshuttle proxy (version %s).' % __version__)
helpers.logprefix = 'c : '
fw = FirewallClient(method_name, sudo_pythonpath)
# If --dns is used, store the IP addresses that the client
# normally uses for DNS lookups in nslist. The firewall needs to
# redirect packets outgoing to this server to the remote host
# nslist is the list of name severs to intercept. If --dns is
# used, we add all DNS servers in resolv.conf. Otherwise, the list
# can be populated with the --ns-hosts option (which is already
# stored in nslist). This list is used to setup the firewall so it
# can redirect packets outgoing to this server to the remote host
# instead.
if dns:
nslist += resolvconf_nameservers(True)
# If we are intercepting DNS requests, we tell the remote host
# where it should send the DNS requests to with the --to-ns
# option.
if len(nslist) > 0:
if to_nameserver is not None:
to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
else:
# option doesn't make sense if we aren't proxying dns
else: # if we are not intercepting DNS traffic
# ...and the user specified a server to send DNS traffic to.
if to_nameserver and len(to_nameserver) > 0:
print("WARNING: --to-ns option is ignored because --dns was not "
"used.")
print("WARNING: --to-ns option is ignored unless "
"--dns or --ns-hosts is used.")
to_nameserver = None
# Get family specific subnet lists. Also, the user may not specify
@ -636,21 +875,22 @@ def main(listenip_v6, listenip_v4,
# listenip_v4 contains user specified value or it is set to "auto".
if listenip_v4 == "auto":
listenip_v4 = ('127.0.0.1', 0)
listenip_v4 = ('127.0.0.1' if avail.loopback_proxy_port else '0.0.0.0', 0)
debug1("Using default IPv4 listen address " + listenip_v4[0])
# listenip_v6 is...
# None when IPv6 is disabled.
# "auto" when listen address is unspecified.
# The user specified address if provided by user
if listenip_v6 is None:
debug1("IPv6 disabled by --disable-ipv6\n")
debug1("IPv6 disabled by --disable-ipv6")
if listenip_v6 == "auto":
if avail.ipv6:
debug1("IPv6 enabled: Using default IPv6 listen address ::1\n")
listenip_v6 = ('::1', 0)
listenip_v6 = ('::1' if avail.loopback_proxy_port else '::', 0)
debug1("IPv6 enabled: Using default IPv6 listen address " + listenip_v6[0])
else:
debug1("IPv6 disabled since it isn't supported by method "
"%s.\n" % fw.method.name)
"%s." % fw.method.name)
listenip_v6 = None
# Make final decision about enabling IPv6:
@ -674,6 +914,15 @@ def main(listenip_v6, listenip_v4,
raise Fatal("User %s does not exist." % user)
required.user = False if user is None else True
if group is not None:
if getgrnam is None:
raise Fatal("Routing by group not available on this system.")
try:
group = getgrnam(group).gr_gid
except KeyError:
raise Fatal("Group %s does not exist." % user)
required.group = False if group is None else True
if not required.ipv6 and len(subnets_v6) > 0:
print("WARNING: IPv6 subnets were ignored because IPv6 is disabled "
"in sshuttle.")
@ -722,9 +971,9 @@ def main(listenip_v6, listenip_v4,
msg += "(available)"
else:
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("IPv6", required.ipv6, avail.ipv6)
feature_status("UDP ", required.udp, avail.udp)
@ -744,20 +993,20 @@ def main(listenip_v6, listenip_v4,
# because we do that below when we have identified the ports to
# listen on.
debug1("Subnets to forward through remote host (type, IP, cidr mask "
"width, startPort, endPort):\n")
"width, startPort, endPort):")
for i in subnets_include:
debug1(" "+str(i)+"\n")
debug1(" "+str(i))
if auto_nets:
debug1("NOTE: Additional subnets to forward may be added below by "
"--auto-nets.\n")
debug1("Subnets to exclude from forwarding:\n")
"--auto-nets.")
debug1("Subnets to exclude from forwarding:")
for i in subnets_exclude:
debug1(" "+str(i)+"\n")
debug1(" "+str(i))
if required.dns:
debug1("DNS requests normally directed at these servers will be "
"redirected to remote:\n")
"redirected to remote:")
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 both ports given, no need to search for a spare port
@ -775,7 +1024,7 @@ def main(listenip_v6, listenip_v4,
redirectport_v4 = 0
bound = False
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()
if required.udp:
@ -818,7 +1067,7 @@ def main(listenip_v6, listenip_v4,
raise e
if not bound:
assert(last_e)
assert last_e
raise last_e
tcp_listener.listen(10)
tcp_listener.print_listening("TCP redirector")
@ -830,7 +1079,7 @@ def main(listenip_v6, listenip_v4,
# search for spare port for DNS
ports = range(12300, 9000, -1)
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:
continue
@ -864,7 +1113,7 @@ def main(listenip_v6, listenip_v4,
dns_listener.print_listening("DNS")
if not bound:
assert(last_e)
assert last_e
raise last_e
else:
dnsport_v6 = 0
@ -903,13 +1152,14 @@ def main(listenip_v6, listenip_v4,
# start the firewall
fw.setup(subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
required.udp, user, tmark)
required.udp, user, group, tmark)
# start the client process
try:
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, dns_listener,
seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver)
python, latency_control, latency_buffer_size,
dns_listener, seed_hosts, auto_hosts, auto_nets,
daemon, to_nameserver, add_cmd_delimiter, remote_shell)
finally:
try:
if daemon:

View File

@ -1,6 +1,8 @@
import os
import re
import shlex
import socket
import platform
import sys
import sshuttle.helpers as helpers
import sshuttle.client as client
import sshuttle.firewall as firewall
@ -9,25 +11,21 @@ import sshuttle.ssyslog as ssyslog
from sshuttle.options import parser, parse_ipport
from sshuttle.helpers import family_ip_tuple, log, Fatal
from sshuttle.sudoers import sudoers
from sshuttle.namespace import enter_namespace
def main():
opt = parser.parse_args()
if 'SSHUTTLE_ARGS' in os.environ:
env_args = shlex.split(os.environ['SSHUTTLE_ARGS'])
else:
env_args = []
args = [*env_args, *sys.argv[1:]]
if opt.sudoers or opt.sudoers_no_modify:
if platform.platform().startswith('OpenBSD'):
log('Automatic sudoers does not work on BSD')
exit(1)
opt = parser.parse_args(args)
if not opt.sudoers_filename:
log('--sudoers-file must be set or omited.')
exit(1)
sudoers(
user_name=opt.sudoers_user,
no_modify=opt.sudoers_no_modify,
file_name=opt.sudoers_filename
)
if opt.sudoers_no_modify:
# sudoers() calls exit() when it completes
sudoers(user_name=opt.sudoers_user)
if opt.daemon:
opt.syslog = 1
@ -40,12 +38,23 @@ def main():
helpers.verbose = opt.verbose
try:
# Since namespace and namespace-pid options are only available
# in linux, we must check if it exists with getattr
namespace = getattr(opt, 'namespace', None)
namespace_pid = getattr(opt, 'namespace_pid', None)
if namespace or namespace_pid:
prefix = helpers.logprefix
helpers.logprefix = 'ns: '
enter_namespace(namespace, namespace_pid)
helpers.logprefix = prefix
if opt.firewall:
if opt.subnets or opt.subnets_file:
parser.error('exactly zero arguments expected')
return firewall.main(opt.method, opt.syslog)
elif opt.hostwatch:
return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
hostwatch.hw_main(opt.subnets, opt.auto_hosts)
return 0
else:
# parse_subnetports() is used to create a list of includes
# and excludes. It is called once for each parameter and
@ -85,6 +94,13 @@ def main():
ipport_v4 = "auto"
# parse_ipport6('[::1]:0')
ipport_v6 = "auto" if not opt.disable_ipv6 else None
try:
int(opt.tmark, 16)
except ValueError:
parser.error("--tmark must be a hexadecimal value")
opt.tmark = opt.tmark.lower() # make 'x' in 0x lowercase
if not opt.tmark.startswith("0x"): # accept without 0x prefix
opt.tmark = "0x%s" % opt.tmark
if opt.syslog:
ssyslog.start_syslog()
ssyslog.close_stdin()
@ -95,6 +111,7 @@ def main():
remotename,
opt.python,
opt.latency_control,
opt.latency_buffer_size,
opt.dns,
nslist,
opt.method,
@ -107,7 +124,10 @@ def main():
opt.to_ns,
opt.pidfile,
opt.user,
opt.group,
opt.sudo_pythonpath,
opt.add_cmd_delimiter,
opt.remote_shell,
opt.tmark)
if return_code == 0:
@ -117,9 +137,9 @@ def main():
return return_code
except Fatal as e:
log('fatal: %s\n' % e)
log('fatal: %s' % e)
return 99
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
log('Keyboard interrupt: exiting.')
return 1

View File

@ -1,17 +1,25 @@
import errno
import shutil
import socket
import signal
import sys
import os
import platform
import traceback
import subprocess as ssubprocess
import base64
import io
import sshuttle.ssyslog as ssyslog
import sshuttle.helpers as helpers
from sshuttle.helpers import debug1, debug2, Fatal
from sshuttle.helpers import is_admin_user, log, debug1, debug2, debug3, Fatal
from sshuttle.methods import get_auto_method, get_method
HOSTSFILE = '/etc/hosts'
if sys.platform == 'win32':
HOSTSFILE = r"C:\Windows\System32\drivers\etc\hosts"
else:
HOSTSFILE = '/etc/hosts'
sshuttle_pid = None
def rewrite_etc_hosts(hostmap, port):
@ -28,7 +36,11 @@ def rewrite_etc_hosts(hostmap, port):
else:
raise
if old_content.strip() and not os.path.exists(BAKFILE):
os.link(HOSTSFILE, BAKFILE)
try:
os.link(HOSTSFILE, BAKFILE)
except OSError:
# file is locked - performing non-atomic copy
shutil.copyfile(HOSTSFILE, BAKFILE)
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
f = open(tmpname, 'w')
for line in old_content.rstrip().split('\n'):
@ -39,45 +51,102 @@ def rewrite_etc_hosts(hostmap, port):
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
f.close()
if st is not None:
os.chown(tmpname, st.st_uid, st.st_gid)
os.chmod(tmpname, st.st_mode)
else:
os.chown(tmpname, 0, 0)
os.chmod(tmpname, 0o600)
os.rename(tmpname, HOSTSFILE)
if sys.platform != 'win32':
if st is not None:
os.chown(tmpname, st.st_uid, st.st_gid)
os.chmod(tmpname, st.st_mode)
else:
os.chown(tmpname, 0, 0)
os.chmod(tmpname, 0o644)
try:
os.rename(tmpname, HOSTSFILE)
except OSError:
# file is locked - performing non-atomic copy
log('Warning: Using a non-atomic way to overwrite %s that can corrupt the file if '
'multiple processes write to it simultaneously.' % HOSTSFILE)
shutil.move(tmpname, HOSTSFILE)
def restore_etc_hosts(hostmap, port):
# Only restore if we added hosts to /etc/hosts previously.
if len(hostmap) > 0:
debug2('undoing /etc/hosts changes.\n')
debug2('undoing /etc/hosts changes.')
rewrite_etc_hosts({}, port)
# Isolate function that needs to be replaced for tests
def setup_daemon():
if os.getuid() != 0:
raise Fatal('fw: '
'You must be root (or enable su/sudo) to set the firewall')
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 interupt signal to sshuttle process %d" % sshuttle_pid)
if sys.platform == 'win32':
sig = signal.CTRL_C_EVENT
else:
sig = signal.SIGINT
os.kill(sshuttle_pid, sig)
def _setup_daemon_for_unix_like():
if not is_admin_user():
raise Fatal('You must have root privileges (or enable su/sudo) to set the firewall')
# don't disappear if our controlling terminal or stdout/stderr
# disappears; we still have to clean up.
signal.signal(signal.SIGHUP, signal.SIG_IGN)
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_IGN)
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGTERM, firewall_exit)
signal.signal(signal.SIGINT, firewall_exit)
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
# I'll die automatically.
os.setsid()
# Calling setsid() here isn't strictly necessary. However, it forces
# Ctrl+C to get sent to the main sshuttle process instead of to
# the firewall process---which is our preferred way to shutdown.
# Nonetheless, if the firewall process receives a SIGTERM/SIGINT
# signal, it will relay a SIGINT to the main sshuttle process
# automatically.
try:
os.setsid()
except OSError:
# setsid() fails if sudo is configured with the use_pty option.
pass
# because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we
# can read from it.
os.dup2(1, 0)
return sys.stdin.buffer, sys.stdout.buffer
return sys.stdin, sys.stdout
def _setup_daemon_for_windows():
if not is_admin_user():
raise Fatal('You must be administrator to set the firewall')
signal.signal(signal.SIGTERM, firewall_exit)
signal.signal(signal.SIGINT, firewall_exit)
com_chan = os.environ.get('SSHUTTLE_FW_COM_CHANNEL')
if com_chan == 'stdio':
debug3('Using inherited stdio for communicating with sshuttle client process')
else:
debug3('Using shared socket for communicating with sshuttle client process')
socket_share_data = base64.b64decode(com_chan)
sock = socket.fromshare(socket_share_data) # type: socket.socket
sys.stdin = io.TextIOWrapper(sock.makefile('rb', buffering=0))
sys.stdout = io.TextIOWrapper(sock.makefile('wb', buffering=0), write_through=True)
sock.close()
return sys.stdin.buffer, sys.stdout.buffer
# Isolate function that needs to be replaced for tests
if sys.platform == 'win32':
setup_daemon = _setup_daemon_for_windows
else:
setup_daemon = _setup_daemon_for_unix_like
# Note that we're sorting in a very particular order:
@ -90,19 +159,50 @@ def subnet_weight(s):
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
def flush_systemd_dns_cache():
# If the user is using systemd-resolve for DNS resolution, it is
# possible for the request to go through systemd-resolve before we
# see it...and it may use a cached result instead of sending a
# request that we can intercept. When sshuttle starts and stops,
# this means that we should clear the cache!
#
# The command to do this was named systemd-resolve, but changed to
# resolvectl in systemd 239.
# https://github.com/systemd/systemd/blob/f8eb41003df1a4eab59ff9bec67b2787c9368dbd/NEWS#L3816
p = None
if helpers.which("resolvectl"):
debug2("Flushing systemd's DNS resolver cache: "
"resolvectl flush-caches")
p = ssubprocess.Popen(["resolvectl", "flush-caches"],
stdout=ssubprocess.PIPE, env=helpers.get_env())
elif helpers.which("systemd-resolve"):
debug2("Flushing systemd's DNS resolver cache: "
"systemd-resolve --flush-caches")
p = ssubprocess.Popen(["systemd-resolve", "--flush-caches"],
stdout=ssubprocess.PIPE, env=helpers.get_env())
if p:
# Wait so flush is finished and process doesn't show up as defunct.
rv = p.wait()
if rv != 0:
log("Received non-zero return code %d when flushing DNS resolver "
"cache." % rv)
# This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch.
#
# This code is supposed to clean up after itself by deleting its rules on
# exit. In case that fails, it's not the end of the world; future runs will
# supercede it in the transproxy list, at least, so the leftover rules
# supersede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless.
def main(method_name, syslog):
helpers.logprefix = 'fw: '
stdin, stdout = setup_daemon()
hostmap = {}
helpers.logprefix = 'fw: '
debug1('Starting firewall with Python version %s\n'
debug1('Starting firewall with Python version %s'
% platform.python_version())
if method_name == "auto":
@ -119,31 +219,45 @@ def main(method_name, syslog):
"Check that the appropriate programs are in your "
"PATH." % method_name)
debug1('ready method name %s.\n' % method.name)
stdout.write('READY %s\n' % method.name)
debug1('ready method name %s.' % method.name)
stdout.write(('READY %s\n' % method.name).encode('ASCII'))
stdout.flush()
def _read_next_string_line():
try:
line = stdin.readline(128)
if not line:
return # parent probably exited
return line.decode('ASCII').strip()
except IOError as e:
# On windows, ConnectionResetError is thrown when parent process closes it's socket pair end
debug3('read from stdin failed: %s' % (e,))
return
# we wait until we get some input before creating the rules. That way,
# sshuttle can launch us as early as possible (and get sudo password
# authentication as early in the startup process as possible).
line = stdin.readline(128)
if not line:
return # parent died; nothing to do
try:
line = _read_next_string_line()
if not line:
return # parent probably exited
except IOError as e:
# On windows, ConnectionResetError is thrown when parent process closes it's socket pair end
debug3('read from stdin failed: %s' % (e,))
return
subnets = []
if line != 'ROUTES\n':
if line != 'ROUTES':
raise Fatal('expected ROUTES but got %r' % line)
while 1:
line = stdin.readline(128)
line = _read_next_string_line()
if not line:
raise Fatal('fw: expected route but got %r' % line)
elif line.startswith("NSLIST\n"):
raise Fatal('expected route but got %r' % line)
elif line.startswith("NSLIST"):
break
try:
(family, width, exclude, ip, fport, lport) = \
line.strip().split(',', 5)
except BaseException:
raise Fatal('fw: expected route or NSLIST but got %r' % line)
(family, width, exclude, ip, fport, lport) = line.split(',', 5)
except Exception:
raise Fatal('expected route or NSLIST but got %r' % line)
subnets.append((
int(family),
int(width),
@ -151,60 +265,63 @@ def main(method_name, syslog):
ip,
int(fport),
int(lport)))
debug2('Got subnets: %r\n' % subnets)
debug2('Got subnets: %r' % subnets)
nslist = []
if line != 'NSLIST\n':
raise Fatal('fw: expected NSLIST but got %r' % line)
if line != 'NSLIST':
raise Fatal('expected NSLIST but got %r' % line)
while 1:
line = stdin.readline(128)
line = _read_next_string_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 "):
break
try:
(family, ip) = line.strip().split(',', 1)
except BaseException:
raise Fatal('fw: expected nslist or PORTS but got %r' % line)
(family, ip) = line.split(',', 1)
except Exception:
raise Fatal('expected nslist or PORTS but got %r' % line)
nslist.append((int(family), ip))
debug2('Got partial nslist: %r\n' % nslist)
debug2('Got nslist: %r\n' % nslist)
debug2('Got partial nslist: %r' % nslist)
debug2('Got nslist: %r' % nslist)
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 = ports.split(",")
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_v4 = int(ports[1])
dnsport_v6 = int(ports[2])
dnsport_v4 = int(ports[3])
assert(port_v6 >= 0)
assert(port_v6 <= 65535)
assert(port_v4 >= 0)
assert(port_v4 <= 65535)
assert(dnsport_v6 >= 0)
assert(dnsport_v6 <= 65535)
assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535)
assert port_v6 >= 0
assert port_v6 <= 65535
assert port_v4 >= 0
assert port_v4 <= 65535
assert dnsport_v6 >= 0
assert dnsport_v6 <= 65535
assert dnsport_v4 >= 0
assert dnsport_v4 <= 65535
debug2('Got ports: %d,%d,%d,%d\n'
debug2('Got ports: %d,%d,%d,%d'
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
line = stdin.readline(128)
if not line:
raise Fatal('fw: expected GO but got %r' % line)
elif not line.startswith("GO "):
raise Fatal('fw: expected GO but got %r' % line)
line = _read_next_string_line()
if not line or not line.startswith("GO "):
raise Fatal('expected GO but got %r' % line)
_, _, args = line.partition(" ")
udp, user = args.strip().split(" ", 1)
global sshuttle_pid
udp, user, group, tmark, sshuttle_pid = args.split(" ", 4)
udp = bool(int(udp))
sshuttle_pid = int(sshuttle_pid)
if user == '-':
user = None
debug2('Got udp: %r, user: %r\n' % (udp, user))
if group == '-':
group = None
debug2('Got udp: %r, user: %r, group: %r, tmark: %s, sshuttle_pid: %d' %
(udp, user, group, tmark, sshuttle_pid))
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
@ -212,83 +329,100 @@ def main(method_name, syslog):
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
try:
debug1('setting up.\n')
debug1('setting up.')
if subnets_v6 or nslist_v6:
debug2('setting up IPv6.\n')
debug2('setting up IPv6.')
method.setup_firewall(
port_v6, dnsport_v6, nslist_v6,
socket.AF_INET6, subnets_v6, udp,
user)
user, group, tmark)
if subnets_v4 or nslist_v4:
debug2('setting up IPv4.\n')
debug2('setting up IPv4.')
method.setup_firewall(
port_v4, dnsport_v4, nslist_v4,
socket.AF_INET, subnets_v4, udp,
user)
stdout.write('STARTED\n')
user, group, tmark)
try:
# For some methods (eg: windivert) firewall setup will be differed / will run asynchronously.
# Such method implements wait_for_firewall_ready() to wait until firewall is up and running.
method.wait_for_firewall_ready(sshuttle_pid)
except NotImplementedError:
pass
if sys.platform == 'linux':
flush_systemd_dns_cache()
try:
stdout.write(b'STARTED\n')
stdout.flush()
except IOError:
# the parent process died for some reason; he's surely been loud
# enough, so no reason to report another error
except IOError as e: # the parent process probably died
debug3('write to stdout failed: %s' % (e,))
return
# Now we wait until EOF or any other kind of exception. We need
# to stay running so that we don't need a *second* password
# authentication at shutdown time - that cleanup is important!
while 1:
line = stdin.readline(128)
line = _read_next_string_line()
if not line:
return
if line.startswith('HOST '):
(name, ip) = line[5:].strip().split(',', 1)
(name, ip) = line[5:].split(',', 1)
hostmap[name] = ip
debug2('setting up /etc/hosts.\n')
debug2('setting up /etc/hosts.')
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
elif line:
if not method.firewall_command(line):
raise Fatal('fw: expected command, got %r' % line)
raise Fatal('expected command, got %r' % line)
else:
break
finally:
try:
debug1('undoing changes.\n')
except BaseException:
debug1('undoing changes.')
except Exception:
debug2('An error occurred, ignoring it.')
try:
if subnets_v6 or nslist_v6:
debug2('undoing IPv6 changes.\n')
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
except BaseException:
debug2('undoing IPv6 changes.')
method.restore_firewall(port_v6, socket.AF_INET6, udp, user, group)
except Exception:
try:
debug1("Error trying to undo IPv6 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("---> %s\n" % line)
except BaseException:
debug1("Error trying to undo IPv6 firewall.")
debug1(traceback.format_exc())
except Exception:
debug2('An error occurred, ignoring it.')
try:
if subnets_v4 or nslist_v4:
debug2('undoing IPv4 changes.\n')
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
except BaseException:
debug2('undoing IPv4 changes.')
method.restore_firewall(port_v4, socket.AF_INET, udp, user, group)
except Exception:
try:
debug1("Error trying to undo IPv4 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("---> %s\n" % line)
except BaseException:
debug1("Error trying to undo IPv4 firewall.")
debug1(traceback.format_exc())
except Exception:
debug2('An error occurred, ignoring it.')
try:
# debug2() message printed in restore_etc_hosts() function.
restore_etc_hosts(hostmap, port_v6 or port_v4)
except BaseException:
except Exception:
try:
debug1("Error trying to undo /etc/hosts changes.\n")
for line in traceback.format_exc().splitlines():
debug1("---> %s\n" % line)
except BaseException:
debug1("Error trying to undo /etc/hosts changes.")
debug1(traceback.format_exc())
except Exception:
debug2('An error occurred, ignoring it.')
if sys.platform == 'linux':
try:
flush_systemd_dns_cache()
except Exception:
try:
debug1("Error trying to flush systemd dns cache.")
debug1(traceback.format_exc())
except Exception:
debug2("An error occurred, ignoring it.")

View File

@ -2,6 +2,13 @@ import sys
import socket
import errno
import os
import threading
import subprocess
import traceback
import re
if sys.platform != "win32":
import fcntl
logprefix = ''
verbose = 0
@ -11,20 +18,28 @@ def b(s):
return s.encode("ASCII")
def get_verbose_level():
return verbose
def log(s):
global logprefix
try:
sys.stdout.flush()
if s.find("\n") != -1:
prefix = logprefix
s = s.rstrip("\n")
for line in s.split("\n"):
sys.stderr.write(prefix + line + "\n")
prefix = "---> "
else:
sys.stderr.write(logprefix + s)
except (IOError, ValueError): # ValueError ~ I/O operation on closed file
pass
try:
# Put newline at end of string if line doesn't have one.
if not s.endswith("\n"):
s = s+"\n"
prefix = logprefix
s = s.rstrip("\n")
for line in s.split("\n"):
sys.stderr.write(prefix + line + "\n")
prefix = " "
sys.stderr.flush()
except IOError:
except (IOError, ValueError): # ValueError ~ I/O operation on closed file
# this could happen if stderr gets forcibly disconnected, eg. because
# our tty closes. That sucks, but it's no reason to abort the program.
pass
@ -91,28 +106,53 @@ def resolvconf_nameservers(systemd_resolved):
words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver':
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]))
nsservers += this_file_nsservers
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))
return nsservers
def resolvconf_random_nameserver(systemd_resolved):
def windows_nameservers():
out = subprocess.check_output(["powershell", "-NonInteractive", "-NoProfile", "-Command", "Get-DnsClientServerAddress"],
encoding="utf-8")
servers = set()
for line in out.splitlines():
if line.startswith("Loopback "):
continue
m = re.search(r'{.+}', line)
if not m:
continue
for s in m.group().strip('{}').split(','):
s = s.strip()
if s.startswith('fec0:0:0:ffff'):
continue
servers.add(s)
debug2("Found DNS servers: %s" % servers)
return [(socket.AF_INET6 if ':' in s else socket.AF_INET, s) for s in servers]
def get_random_nameserver():
"""Return a random nameserver selected from servers produced by
resolvconf_nameservers(). See documentation for
resolvconf_nameservers() for a description of the parameter.
resolvconf_nameservers()/windows_nameservers()
"""
lines = resolvconf_nameservers(systemd_resolved)
if lines:
if len(lines) > 1:
if sys.platform == "win32":
if globals().get('_nameservers') is None:
ns_list = windows_nameservers()
globals()['_nameservers'] = ns_list
else:
ns_list = globals()['_nameservers']
else:
ns_list = resolvconf_nameservers(systemd_resolved=False)
if ns_list:
if len(ns_list) > 1:
# don't import this unless we really need it
import random
random.shuffle(lines)
return lines[0]
random.shuffle(ns_list)
return ns_list[0]
else:
return (socket.AF_INET, '127.0.0.1')
@ -215,7 +255,95 @@ def which(file, mode=os.F_OK | os.X_OK):
path = get_path()
rv = _which(file, mode, path)
if rv:
debug2("which() found '%s' at %s\n" % (file, rv))
debug2("which() found '%s' at %s" % (file, rv))
else:
debug2("which() could not find '%s' in %s\n" % (file, path))
debug2("which() could not find '%s' in %s" % (file, path))
return rv
def is_admin_user():
if sys.platform == 'win32':
# https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script/41930586#41930586
import ctypes
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except Exception:
return False
# TODO(nom3ad): for sys.platform == 'linux', check capabilities for non-root users. (CAP_NET_ADMIN might be enough?)
return os.getuid() == 0
def set_non_blocking_io(fd):
if sys.platform != "win32":
try:
os.set_blocking(fd, False)
except AttributeError:
# python < 3.5
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
flags |= os.O_NONBLOCK
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
else:
_sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
_sock.setblocking(False)
class RWPair:
def __init__(self, r, w):
self.r = r
self.w = w
self.read = r.read
self.readline = r.readline
self.write = w.write
self.flush = w.flush
def close(self):
for f in self.r, self.w:
try:
f.close()
except Exception:
pass
class SocketRWShim:
__slots__ = ('_r', '_w', '_on_end', '_s1', '_s2', '_t1', '_t2')
def __init__(self, r, w, on_end=None):
self._r = r
self._w = w
self._on_end = on_end
self._s1, self._s2 = socket.socketpair()
debug3("[SocketShim] r=%r w=%r | s1=%r s2=%r" % (self._r, self._w, self._s1, self._s2))
def stream_reader_to_sock():
try:
for data in iter(lambda: self._r.read(16384), b''):
self._s1.sendall(data)
# debug3("[SocketRWShim] <<<<< r.read() %d %r..." % (len(data), data[:min(32, len(data))]))
except Exception:
traceback.print_exc(file=sys.stderr)
finally:
debug2("[SocketRWShim] Thread 'stream_reader_to_sock' exiting")
self._s1.close()
self._on_end and self._on_end()
def stream_sock_to_writer():
try:
for data in iter(lambda: self._s1.recv(16384), b''):
while data:
n = self._w.write(data)
data = data[n:]
# debug3("[SocketRWShim] <<<<< w.write() %d %r..." % (len(data), data[:min(32, len(data))]))
except Exception:
traceback.print_exc(file=sys.stderr)
finally:
debug2("[SocketRWShim] Thread 'stream_sock_to_writer' exiting")
self._s1.close()
self._on_end and self._on_end()
self._t1 = threading.Thread(target=stream_reader_to_sock, name='stream_reader_to_sock', daemon=True).start()
self._t2 = threading.Thread(target=stream_sock_to_writer, name='stream_sock_to_writer', daemon=True).start()
def makefiles(self):
return self._s2.makefile("rb", buffering=0), self._s2.makefile("wb", buffering=0)

View File

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

View File

@ -7,7 +7,7 @@ def nonfatal(func, *args):
try:
func(*args)
except Fatal as e:
log('fw: error: %s\n' % e)
log('error: %s' % e)
def ipt_chain_exists(family, table, name):
@ -17,27 +17,27 @@ def ipt_chain_exists(family, table, name):
cmd = 'iptables'
else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
argv = [cmd, '-t', table, '-nL']
argv = [cmd, '-w', '-t', table, '-nL']
try:
output = ssubprocess.check_output(argv, env=get_env())
for line in output.decode('ASCII').split('\n'):
for line in output.decode('ASCII', errors='replace').split('\n'):
if line.startswith('Chain %s ' % name):
return True
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):
if family == socket.AF_INET6:
argv = ['ip6tables', '-t', table] + list(args)
argv = ['ip6tables', '-w', '-t', table] + list(args)
elif family == socket.AF_INET:
argv = ['iptables', '-t', table] + list(args)
argv = ['iptables', '-w', '-t', table] + list(args)
else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
debug1('%s\n' % ' '.join(argv))
debug1('%s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env())
if rv:
raise Fatal('fw: %r returned %d' % (argv, rv))
raise Fatal('%r returned %d' % (argv, rv))
def nft(family, table, action, *args):
@ -45,29 +45,7 @@ def nft(family, table, action, *args):
argv = ['nft', action, 'inet', table] + list(args)
else:
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())
if rv:
raise Fatal('fw: %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)
raise Fatal('%r returned %d' % (argv, rv))

View File

@ -1,14 +1,13 @@
import importlib
import socket
import struct
import sys
import errno
import ipaddress
from sshuttle.helpers import Fatal, debug3
def original_dst(sock):
ip = "0.0.0.0"
port = -1
try:
family = sock.family
SO_ORIGINAL_DST = 80
@ -47,11 +46,13 @@ class BaseMethod(object):
@staticmethod
def get_supported_features():
result = Features()
result.loopback_proxy_port = True
result.ipv4 = True
result.ipv6 = False
result.udp = False
result.dns = True
result.user = False
result.group = False
return result
@staticmethod
@ -66,14 +67,14 @@ class BaseMethod(object):
@staticmethod
def recv_udp(udp_listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
debug3('Accept UDP using recvfrom.')
data, srcip = udp_listener.recvfrom(bufsize)
return (srcip, None, data)
def send_udp(self, sock, srcip, dstip, data):
if srcip is not None:
Fatal("Method %s send_udp does not support setting srcip to %r"
% (self.name, srcip))
raise Fatal("Method %s send_udp does not support setting srcip to %r"
% (self.name, srcip))
sock.sendto(data, dstip)
def setup_tcp_listener(self, tcp_listener):
@ -87,14 +88,17 @@ class BaseMethod(object):
for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
if getattr(features, key) and not getattr(avail, key):
raise Fatal(
"Feature %s not supported with method %s.\n" %
"Feature %s not supported with method %s." %
(key, self.name))
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
user, group, tmark):
raise NotImplementedError()
def restore_firewall(self, port, family, udp, user):
def restore_firewall(self, port, family, udp, user, group):
raise NotImplementedError()
def wait_for_firewall_ready(self, sshuttle_pid):
raise NotImplementedError()
@staticmethod
@ -108,13 +112,13 @@ def get_method(method_name):
def get_auto_method():
debug3("Selecting a method automatically...\n")
debug3("Selecting a method automatically...")
# Try these methods, in order:
methods_to_try = ["nat", "nft", "pf", "ipfw"]
methods_to_try = ["nat", "nft", "pf", "ipfw"] if sys.platform != "win32" else ["windivert"]
for m in methods_to_try:
method = get_method(m)
if method.is_supported():
debug3("Method '%s' was automatically selected.\n" % m)
debug3("Method '%s' was automatically selected." % m)
return method
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, \
Fatal, family_to_string, get_env, which
recvmsg = None
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
recvmsg = "python"
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
recvmsg = "socket_ext"
except ImportError:
import socket
import socket
IP_BINDANY = 24
IP_RECVDSTADDR = 7
SOL_IPV6 = 41
IPV6_RECVDSTADDR = 74
if recvmsg == "python":
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n')
data, ancdata, _, srcip = listener.recvmsg(4096,
socket.CMSG_SPACE(4))
dstip = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
port = 53
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
dstip = (ip, port)
break
return (srcip, dstip, data)
elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize):
debug3('Accept UDP using socket_ext recvmsg.\n')
srcip, data, adata, _ = listener.recvmsg((bufsize,),
socket.CMSG_SPACE(4))
dstip = None
for a in adata:
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR:
port = 53
ip = socket.inet_ntop(socket.AF_INET, a.cmsg_data[0:4])
dstip = (ip, port)
break
return (srcip, dstip, data[0])
else:
def recv_udp(listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = listener.recvfrom(bufsize)
return (srcip, None, data)
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.')
data, ancdata, _, srcip = listener.recvmsg(4096,
socket.CMSG_SPACE(4))
dstip = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
port = 53
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
dstip = (ip, port)
break
return (srcip, dstip, data)
def ipfw_rule_exists(n):
argv = ['ipfw', 'list']
argv = ['ipfw', 'list', '%d' % n]
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
found = False
for line in p.stdout:
if line.startswith(b'%05d ' % n):
if not ('ipttl 63' in line or 'check-state' in line):
log('non-sshuttle ipfw rule: %r\n' % line.strip())
if 'check-state :sshuttle' not in line:
log('non-sshuttle ipfw rule: %r' % line.strip())
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
found = True
break
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
@ -84,7 +52,7 @@ def _fill_oldctls(prefix):
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
for line in p.stdout:
line = line.decode()
assert(line[-1] == '\n')
assert line[-1] == '\n'
(k, v) = line[:-1].split(': ', 1)
_oldctls[k] = v.strip()
rv = p.wait()
@ -96,7 +64,7 @@ def _fill_oldctls(prefix):
def _sysctl_set(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())
# No env: No output. (Or error that won't be parsed.)
@ -106,18 +74,18 @@ _changedctls = []
def sysctl_set(name, val, permanent=False):
PREFIX = 'net.inet.ip'
assert(name.startswith(PREFIX + '.'))
assert name.startswith(PREFIX + '.')
val = str(val)
if not _oldctls:
_fill_oldctls(PREFIX)
if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name)
debug1('>> No such sysctl: %r' % name)
return False
oldval = _oldctls[name]
if val != oldval:
rv = _sysctl_set(name, val)
if rv == 0 and permanent:
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
debug1('>> ...saving permanently in /etc/sysctl.conf')
f = open('/etc/sysctl.conf', 'a')
f.write('\n'
'# Added by sshuttle\n'
@ -130,7 +98,7 @@ def sysctl_set(name, val, permanent=False):
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
debug1('>> %s' % ' '.join(argv))
rv = ssubprocess.call(argv, env=get_env())
# No env: No output. (Or error that won't be parsed.)
if rv:
@ -139,7 +107,7 @@ def ipfw(*args):
def ipfw_noexit(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
debug1('>> %s' % ' '.join(argv))
ssubprocess.call(argv, env=get_env())
# No env: No output. (Or error that won't be parsed.)
@ -161,7 +129,7 @@ class Method(BaseMethod):
if not dstip:
debug1(
"-- ignored UDP from %r: "
"couldn't determine destination IP address\n" % (srcip,))
"couldn't determine destination IP address" % (srcip,))
return None
return srcip, dstip, data
@ -169,15 +137,14 @@ class Method(BaseMethod):
if not srcip:
debug1(
"-- ignored UDP to %r: "
"couldn't determine source IP address\n" % (dstip,))
"couldn't determine source IP address" % (dstip,))
return
# debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
# debug3('Sending SRC: %r DST: %r' % (srcip, dstip))
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
sender.bind(srcip)
sender.sendto(data, dstip)
sender.close()
@ -189,7 +156,7 @@ class Method(BaseMethod):
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
user, group, tmark):
# IPv6 not supported
if family not in [socket.AF_INET]:
raise Exception(
@ -207,8 +174,7 @@ class Method(BaseMethod):
if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1)
ipfw('add', '1', 'check-state', 'ip',
'from', 'any', 'to', 'any')
ipfw('add', '1', 'check-state', ':sshuttle')
ipfw('add', '1', 'skipto', '2',
'tcp',
@ -216,7 +182,7 @@ class Method(BaseMethod):
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
'tcp',
'from', 'any', 'to', 'table(126)',
'not', 'ipttl', '63', 'keep-state', 'setup')
'setup', 'keep-state', ':sshuttle')
ipfw_noexit('table', '124', 'flush')
dnscount = 0
@ -227,26 +193,24 @@ class Method(BaseMethod):
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
'udp',
'from', 'any', 'to', 'table(124)',
'not', 'ipttl', '63')
'keep-state', ':sshuttle')
ipfw('add', '1', 'allow',
'udp',
'from', 'any', 'to', 'any',
'ipttl', '63')
'from', 'any', 'to', 'any')
if subnets:
# create new subnet entries
for _, swidth, sexclude, snet in sorted(subnets,
key=lambda s: s[1],
reverse=True):
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
else:
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
else:
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
def restore_firewall(self, port, family, udp, user):
def restore_firewall(self, port, family, udp, user, group):
if family not in [socket.AF_INET]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
'Address family "%s" unsupported by ipfw method'
% family_to_string(family))
ipfw_noexit('delete', '1')
@ -258,5 +222,5 @@ class Method(BaseMethod):
if which("ipfw"):
return True
debug2("ipfw method not supported because 'ipfw' command is "
"missing.\n")
"missing.")
return False

View File

@ -1,7 +1,7 @@
import socket
from sshuttle.firewall import subnet_weight
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
@ -13,36 +13,36 @@ class Method(BaseMethod):
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
# only ipv4 supported with NAT
if family != socket.AF_INET:
user, group, tmark):
if family != socket.AF_INET and family != socket.AF_INET6:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by nat method_name")
table = "nat"
def _ipt(*args):
return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipm(*args):
return ipt(family, "mangle", *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
self.restore_firewall(port, family, udp, user)
self.restore_firewall(port, family, udp, user, group)
_ipt('-N', chain)
_ipt('-F', chain)
if user is not None:
_ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user),
'-j', 'MARK', '--set-mark', str(port))
if user is not None or group is not None:
margs = ['-I', 'OUTPUT', '1', '-m', 'owner']
if user is not None:
margs += ['--uid-owner', str(user)]
if group is not None:
margs += ['--gid-owner', str(group)]
margs += ['-j', 'MARK', '--set-mark', str(port)]
nonfatal(_ipm, *margs)
args = '-m', 'mark', '--mark', str(port), '-j', chain
else:
args = '-j', chain
@ -50,25 +50,15 @@ class Method(BaseMethod):
_ipt('-I', 'OUTPUT', '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
# to localhost DNS servers through sshuttle.
for _, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip,
'--dest', '%s' % ip,
'-p', 'udp',
'--dport', '53',
'--to-ports', str(dnsport))
# Don't route any remaining local traffic through sshuttle.
_ipt('-A', chain, '-j', 'RETURN',
'-m', 'addrtype',
'--dst-type', 'LOCAL')
# create new subnet entries.
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True):
@ -85,9 +75,14 @@ class Method(BaseMethod):
'--dest', '%s/%s' % (snet, swidth),
*(tcp_ports + ('--to-ports', str(port))))
def restore_firewall(self, port, family, udp, user):
# Don't route any remaining local traffic through sshuttle.
_ipt('-A', chain, '-j', 'RETURN',
'-m', 'addrtype',
'--dst-type', 'LOCAL')
def restore_firewall(self, port, family, udp, user, group):
# only ipv4 supported with NAT
if family != socket.AF_INET:
if family != socket.AF_INET and family != socket.AF_INET6:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
@ -99,9 +94,6 @@ class Method(BaseMethod):
def _ipt(*args):
return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipm(*args):
return ipt(family, "mangle", *args)
@ -109,9 +101,15 @@ class Method(BaseMethod):
# basic cleanup/setup of chains
if ipt_chain_exists(family, table, chain):
if user is not None:
nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner',
str(user), '-j', 'MARK', '--set-mark', str(port))
if user is not None or group is not None:
margs = ['-D', 'OUTPUT', '-m', 'owner']
if user is not None:
margs += ['--uid-owner', str(user)]
if group is not None:
margs += ['--gid-owner', str(group)]
margs += ['-j', 'MARK', '--set-mark', str(port)]
nonfatal(_ipm, *margs)
args = '-m', 'mark', '--mark', str(port), '-j', chain
else:
args = '-j', chain
@ -123,11 +121,13 @@ class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.user = True
result.ipv6 = True
result.group = True
return result
def is_supported(self):
if which("iptables"):
return True
debug2("nat method not supported because 'iptables' command "
"is missing.\n")
"is missing.")
return False

View File

@ -13,7 +13,7 @@ class Method(BaseMethod):
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
user, group, tmark):
if udp:
raise Exception("UDP not supported by nft")
@ -45,14 +45,6 @@ class Method(BaseMethod):
else:
_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
if family == socket.AF_INET:
ip_version_l = 'ipv4'
@ -95,7 +87,7 @@ class Method(BaseMethod):
ip_version, 'daddr %s/%s' % (snet, swidth),
('redirect to :' + str(port)))))
def restore_firewall(self, port, family, udp, user):
def restore_firewall(self, port, family, udp, user, group):
if udp:
raise Exception("UDP not supported by nft method_name")
@ -118,5 +110,5 @@ class Method(BaseMethod):
def is_supported(self):
if which("nft"):
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

View File

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

View File

@ -1,26 +1,12 @@
import struct
from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
from sshuttle.linux import ipt, ipt_chain_exists
from sshuttle.methods import BaseMethod
from sshuttle.helpers import debug1, debug2, debug3, Fatal, which
recvmsg = None
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
recvmsg = "python"
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
recvmsg = "socket_ext"
except ImportError:
import socket
import socket
import os
IP_TRANSPARENT = 19
@ -30,75 +16,37 @@ SOL_IPV6 = 41
IPV6_ORIGDSTADDR = 74
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
if recvmsg == "python":
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n')
data, ancdata, _, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24))
dstip = None
family = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
return (srcip, dstip, data)
elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize):
debug3('Accept UDP using socket_ext recvmsg.\n')
srcip, data, adata, _ = listener.recvmsg(
(bufsize,), socket.CMSG_SPACE(24))
dstip = None
family = None
for a in adata:
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
dstip = (ip, port)
break
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
dstip = (ip, port)
break
return (srcip, dstip, data[0])
else:
def recv_udp(listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = listener.recvfrom(bufsize)
return (srcip, None, data)
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.')
data, ancdata, _, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24))
dstip = None
family = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
return (srcip, dstip, data)
class Method(BaseMethod):
@ -106,12 +54,8 @@ class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = True
if recvmsg is None:
result.udp = False
result.dns = False
else:
result.udp = True
result.dns = True
result.udp = True
result.dns = True
return result
def get_tcp_dstip(self, sock):
@ -126,6 +70,15 @@ class Method(BaseMethod):
return None
return srcip, dstip, data
def setsockopt_error(self, e):
"""The tproxy method needs root permissions to successfully
set the IP_TRANSPARENT option on sockets. This method is
called when we receive a PermissionError when trying to do
so."""
raise Fatal("Insufficient permissions for tproxy method.\n"
"Your effective UID is %d, not 0. Try rerunning as root.\n"
% os.geteuid())
def send_udp(self, sock, srcip, dstip, data):
if not srcip:
debug1(
@ -134,16 +87,26 @@ class Method(BaseMethod):
return
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
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.sendto(data, dstip)
sender.close()
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):
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:
udp_listener.v4.setsockopt(
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
@ -151,17 +114,7 @@ class Method(BaseMethod):
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user):
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):
user, group, tmark):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
@ -172,19 +125,16 @@ class Method(BaseMethod):
def _ipt(*args):
return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
def _ipt_proto_ports(proto, fport, lport):
return proto + ('--dport', '%d:%d' % (fport, lport)) \
if fport else proto
if fport else proto
mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port
# basic cleanup/setup of chains
self.restore_firewall(port, family, udp, user)
self.restore_firewall(port, family, udp, user, group)
_ipt('-N', mark_chain)
_ipt('-F', mark_chain)
@ -192,8 +142,34 @@ class Method(BaseMethod):
_ipt('-F', divert_chain)
_ipt('-N', tproxy_chain)
_ipt('-F', tproxy_chain)
_ipt('-I', 'OUTPUT', tmark, '-j', mark_chain)
_ipt('-I', 'PREROUTING', tmark, '-j', tproxy_chain)
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
for _, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', tmark,
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
# Don't have packets sent to any of our local IP addresses go
# through the tproxy or mark chains (except DNS ones).
#
# Without this fix, if a large subnet is redirected through
# sshuttle (i.e., 0/0), then the user may be unable to receive
# UDP responses or connect to their own machine using an IP
# besides (127.0.0.1). Prior to including these lines, the
# documentation reminded the user to use -x to exclude their
# own IP addresses to receive UDP responses if they are
# redirecting a large subnet through sshuttle (i.e., 0/0).
_ipt('-A', tproxy_chain, '-j', 'RETURN', '-m', 'addrtype',
'--dst-type', 'LOCAL')
_ipt('-A', mark_chain, '-j', 'RETURN', '-m', 'addrtype',
'--dst-type', 'LOCAL')
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', tmark)
_ipt('-A', divert_chain, '-j', 'ACCEPT')
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
@ -203,16 +179,6 @@ class Method(BaseMethod):
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'udp', '-p', 'udp')
for _, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x'+tmark+'/0x'+tmark,
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True):
tcp_ports = ('-p', 'tcp')
@ -233,7 +199,7 @@ class Method(BaseMethod):
'-m', 'tcp',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x'+tmark+'/0x'+tmark,
'--tproxy-mark', tmark,
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp',
*(tcp_ports + ('--on-port', str(port))))
@ -257,12 +223,12 @@ class Method(BaseMethod):
'-m', 'udp',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x'+tmark+'/0x'+tmark,
'--tproxy-mark', tmark,
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp',
*(udp_ports + ('--on-port', str(port))))
def restore_firewall(self, port, family, udp, user):
def restore_firewall(self, port, family, udp, user, group):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
@ -273,9 +239,6 @@ class Method(BaseMethod):
def _ipt(*args):
return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port

View File

@ -0,0 +1,533 @@
import os
import sys
from ipaddress import ip_address, ip_network
import threading
from collections import namedtuple
import socket
import subprocess
import re
from multiprocessing import shared_memory
from struct import Struct
from functools import wraps
from enum import IntEnum
import time
import traceback
from sshuttle.methods import BaseMethod
from sshuttle.helpers import log, debug3, debug1, debug2, get_verbose_level, Fatal
try:
# https://reqrypt.org/windivert-doc.html#divert_iphdr
# https://www.reqrypt.org/windivert-changelog.txt
import pydivert
except ImportError:
raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert")
ConnectionTuple = namedtuple(
"ConnectionTuple",
["protocol", "ip_version", "src_addr", "src_port", "dst_addr", "dst_port", "state_epoch", "state"],
)
WINDIVERT_MAX_CONNECTIONS = int(os.environ.get('WINDIVERT_MAX_CONNECTIONS', 1024))
class IPProtocol(IntEnum):
TCP = socket.IPPROTO_TCP
UDP = socket.IPPROTO_UDP
@property
def filter(self):
return "tcp" if self == IPProtocol.TCP else "udp"
class IPFamily(IntEnum):
IPv4 = socket.AF_INET
IPv6 = socket.AF_INET6
@staticmethod
def from_ip_version(version):
return IPFamily.IPv6 if version == 4 else IPFamily.IPv4
@property
def filter(self):
return "ip" if self == socket.AF_INET else "ipv6"
@property
def version(self):
return 4 if self == socket.AF_INET else 6
@property
def loopback_addr(self):
return ip_address("127.0.0.1" if self == socket.AF_INET else "::1")
class ConnState(IntEnum):
TCP_SYN_SENT = 11 # SYN sent
TCP_ESTABLISHED = 12 # SYN+ACK received
TCP_FIN_WAIT_1 = 91 # FIN sent
TCP_CLOSE_WAIT = 92 # FIN received
@staticmethod
def can_timeout(state):
return state in (ConnState.TCP_SYN_SENT, ConnState.TCP_FIN_WAIT_1, ConnState.TCP_CLOSE_WAIT)
def repr_pkt(p):
try:
direction = p.direction.name
if p.is_loopback:
direction += "/lo"
except AttributeError: # windiver > 2.0
direction = 'OUT' if p.address.Outbound == 1 else 'IN'
if p.address.Loopback == 1:
direction += '/lo'
r = f"{direction} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}"
if p.tcp:
t = p.tcp
r += f" {len(t.payload)}B ("
r += "+".join(
f.upper() for f in ("fin", "syn", "rst", "psh", "ack", "urg", "ece", "cwr", "ns") if getattr(t, f)
)
r += f") SEQ#{t.seq_num}"
if t.ack:
r += f" ACK#{t.ack_num}"
r += f" WZ={t.window_size}"
else:
r += f" {p.udp=} {p.icmpv4=} {p.icmpv6=}"
return f"<Pkt {r}>"
def synchronized_method(lock):
def decorator(method):
@wraps(method)
def wrapped(self, *args, **kwargs):
with getattr(self, lock):
return method(self, *args, **kwargs)
return wrapped
return decorator
class ConnTrack:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = object.__new__(cls)
return cls._instance
raise RuntimeError("ConnTrack can not be instantiated multiple times")
def __init__(self, name, max_connections=0) -> None:
self.struct_full_tuple = Struct(">" + "".join(("B", "B", "16s", "H", "16s", "H", "L", "B")))
self.struct_src_tuple = Struct(">" + "".join(("B", "B", "16s", "H")))
self.struct_state_tuple = Struct(">" + "".join(("L", "B")))
try:
self.max_connections = max_connections
self.shm_list = shared_memory.ShareableList(
[bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name
)
self.is_owner = True
self.next_slot = 0
self.used_slots = set()
self.rlock = threading.RLock()
except FileExistsError:
self.is_owner = False
self.shm_list = shared_memory.ShareableList(name=name)
self.max_connections = len(self.shm_list)
debug2(
f"ConnTrack: is_owner={self.is_owner} cap={len(self.shm_list)} item_sz={self.struct_full_tuple.size}B"
f"shm_name={self.shm_list.shm.name} shm_sz={self.shm_list.shm.size}B"
)
@synchronized_method("rlock")
def add(self, proto, src_addr, src_port, dst_addr, dst_port, state):
if not self.is_owner:
raise RuntimeError("Only owner can mutate ConnTrack")
if len(self.used_slots) >= self.max_connections:
raise RuntimeError(f"No slot available in ConnTrack {len(self.used_slots)}/{self.max_connections}")
if self.get(proto, src_addr, src_port):
return
for _ in range(self.max_connections):
if self.next_slot not in self.used_slots:
break
self.next_slot = (self.next_slot + 1) % self.max_connections
else:
raise RuntimeError("No slot available in ConnTrack") # should not be here
src_addr = ip_address(src_addr)
dst_addr = ip_address(dst_addr)
assert src_addr.version == dst_addr.version
ip_version = src_addr.version
state_epoch = int(time.time())
entry = (proto, ip_version, src_addr.packed, src_port, dst_addr.packed, dst_port, state_epoch, state)
packed = self.struct_full_tuple.pack(*entry)
self.shm_list[self.next_slot] = packed
self.used_slots.add(self.next_slot)
proto = IPProtocol(proto)
debug3(
f"ConnTrack: added ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to "
f"slot={self.next_slot} | #ActiveConn={len(self.used_slots)}"
)
@synchronized_method("rlock")
def update(self, proto, src_addr, src_port, state):
if not self.is_owner:
raise RuntimeError("Only owner can mutate ConnTrack")
src_addr = ip_address(src_addr)
packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port)
for i in self.used_slots:
if self.shm_list[i].startswith(packed):
state_epoch = int(time.time())
self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state)
debug3(
f"ConnTrack: updated ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | "
f"#ActiveConn={len(self.used_slots)}"
)
return self._unpack(self.shm_list[i])
else:
debug3(
f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | "
f"#ActiveConn={len(self.used_slots)}"
)
@synchronized_method("rlock")
def remove(self, proto, src_addr, src_port):
if not self.is_owner:
raise RuntimeError("Only owner can mutate ConnTrack")
src_addr = ip_address(src_addr)
packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port)
for i in self.used_slots:
if self.shm_list[i].startswith(packed):
conn = self._unpack(self.shm_list[i])
self.shm_list[i] = b""
self.used_slots.remove(i)
debug3(
f"ConnTrack: removed ({proto.name} src={src_addr}:{src_port} state={conn.state.name}) from slot={i} | "
f"#ActiveConn={len(self.used_slots)}"
)
return conn
else:
debug3(
f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to remove |"
f" #ActiveConn={len(self.used_slots)}"
)
def get(self, proto, src_addr, src_port):
src_addr = ip_address(src_addr)
packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port)
for entry in self.shm_list:
if entry and entry.startswith(packed):
return self._unpack(entry)
def dump(self):
for entry in self.shm_list:
if not entry:
continue
conn = self._unpack(entry)
proto, ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, state = conn
log(f"{proto.name}/{ip_version} {src_addr}:{src_port} -> {dst_addr}:{dst_port} {state.name}@{state_epoch}")
@synchronized_method("rlock")
def gc(self, connection_timeout_sec=15):
# self.dump()
now = int(time.time())
n = 0
for i in tuple(self.used_slots):
state_packed = self.shm_list[i][-5:]
(state_epoch, state) = self.struct_state_tuple.unpack(state_packed)
if (now - state_epoch) < connection_timeout_sec:
continue
if ConnState.can_timeout(state):
conn = self._unpack(self.shm_list[i])
self.shm_list[i] = b""
self.used_slots.remove(i)
n += 1
debug3(
f"ConnTrack: GC: removed ({conn.protocol.name} src={conn.src_addr}:{conn.src_port} state={conn.state.name})"
f" from slot={i} | #ActiveConn={len(self.used_slots)}"
)
debug3(f"ConnTrack: GC: collected {n} connections | #ActiveConn={len(self.used_slots)}")
def _unpack(self, packed):
(
proto,
ip_version,
src_addr_packed,
src_port,
dst_addr_packed,
dst_port,
state_epoch,
state,
) = self.struct_full_tuple.unpack(packed)
dst_addr = ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4]).exploded
src_addr = ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4]).exploded
proto = IPProtocol(proto)
state = ConnState(state)
return ConnectionTuple(proto, ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, state)
def __iter__(self):
def conn_iter():
for i in self.used_slots:
yield self._unpack(self.shm_list[i])
return conn_iter()
def __repr__(self):
return f"<ConnTrack(n={len(self.used_slots) if self.is_owner else '?'},cap={len(self.shm_list)},owner={self.is_owner})>"
class Method(BaseMethod):
network_config = {}
def __init__(self, name):
super().__init__(name)
def _get_bind_address_for_port(self, port, family):
proto = "TCPv6" if family.version == 6 else "TCP"
for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode(errors='ignore').splitlines():
try:
_, local_addr, _, state, *_ = re.split(r"\s+", line.strip())
except ValueError:
continue
port_suffix = ":" + str(port)
if state == "LISTENING" and local_addr.endswith(port_suffix):
return ip_address(local_addr[:-len(port_suffix)].strip("[]"))
raise Fatal("Could not find listening address for {}/{}".format(port, proto))
def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user, group, tmark):
debug2(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {group=} {tmark=}")
if nslist or user or udp or group:
raise NotImplementedError("user, group, nslist, udp are not supported")
family = IPFamily(family)
proxy_ip = None
# using loopback only proxy binding won't work with windivert.
# See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 https://github.com/basil00/Divert/issues/82)
# As a workaround, finding another interface ip instead. (client should not bind proxy to loopback address)
proxy_bind_addr = self._get_bind_address_for_port(proxy_port, family)
if proxy_bind_addr.is_loopback:
raise Fatal("Windivert method requires proxy to be reachable by a non loopback address.")
if not proxy_bind_addr.is_unspecified:
proxy_ip = proxy_bind_addr
else:
local_addresses = [ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), 0, family=family)]
for addr in local_addresses:
if not addr.is_loopback and not addr.is_link_local:
proxy_ip = addr
break
else:
raise Fatal("Windivert method requires proxy to be reachable by a non loopback address."
f"No address found for {family.name} in {local_addresses}")
debug2(f"Found non loopback address to connect to proxy: {proxy_ip}")
subnet_addresses = []
for (_, mask, exclude, network_addr, fport, lport) in subnets:
if fport and lport:
if lport > fport:
raise Fatal("lport must be less than or equal to fport")
ports = (fport, lport)
else:
ports = None
subnet_addresses.append((ip_network(f"{network_addr}/{mask}"), ports, exclude))
self.network_config[family] = {
"subnets": subnet_addresses,
"nslist": nslist,
"proxy_addr": (proxy_ip, proxy_port)
}
def wait_for_firewall_ready(self, sshuttle_pid):
debug2(f"network_config={self.network_config}")
self.conntrack = ConnTrack(f"sshuttle-windivert-{sshuttle_pid}", WINDIVERT_MAX_CONNECTIONS)
if not self.conntrack.is_owner:
raise Fatal("ConnTrack should be owner in wait_for_firewall_ready()")
thread_target_funcs = (self._egress_divert, self._ingress_divert, self._connection_gc)
ready_events = []
for fn in thread_target_funcs:
ev = threading.Event()
ready_events.append(ev)
def _target():
try:
fn(ev.set)
except Exception:
debug2(f"thread {fn.__name__} exiting due to: " + traceback.format_exc())
sys.stdin.close() # this will exist main thread
sys.stdout.close()
threading.Thread(name=fn.__name__, target=_target, daemon=True).start()
for ev in ready_events:
if not ev.wait(5): # at most 5 sec
raise Fatal("timeout in wait_for_firewall_ready()")
def restore_firewall(self, port, family, udp, user, group):
pass
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.loopback_proxy_port = False
result.user = False
result.dns = False
# ipv6 only able to support with Windivert 2.x due to bugs in filter parsing
# TODO(nom3ad): Enable ipv6 once https://github.com/ffalcinelli/pydivert/pull/57 merged
result.ipv6 = False
return result
def get_tcp_dstip(self, sock):
if not hasattr(self, "conntrack"):
self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getpid()}")
if self.conntrack.is_owner:
raise Fatal("ConnTrack should not be owner in get_tcp_dstip()")
src_addr, src_port = sock.getpeername()
c = self.conntrack.get(IPProtocol.TCP, src_addr, src_port)
if not c:
return (src_addr, src_port)
return (c.dst_addr, c.dst_port)
def is_supported(self):
if sys.platform == "win32":
return True
return False
def _egress_divert(self, ready_cb):
"""divert outgoing packets to proxy"""
proto = IPProtocol.TCP
filter = f"outbound and {proto.filter}"
af_filters = []
for af, c in self.network_config.items():
subnet_include_filters = []
subnet_exclude_filters = []
for ip_net, ports, exclude in c["subnets"]:
first_ip = ip_net.network_address.exploded
last_ip = ip_net.broadcast_address.exploded
if first_ip == last_ip:
_subnet_filter = f"{af.filter}.DstAddr=={first_ip}"
else:
_subnet_filter = f"{af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip}"
if ports:
if ports[0] == ports[1]:
_subnet_filter += f" and {proto.filter}.DstPort=={ports[0]}"
else:
_subnet_filter += f" and tcp.DstPort>={ports[0]} and tcp.DstPort<={ports[1]}"
(subnet_exclude_filters if exclude else subnet_include_filters).append(f"({_subnet_filter})")
_af_filter = f"{af.filter}"
if subnet_include_filters:
_af_filter += f" and ({' or '.join(subnet_include_filters)})"
if subnet_exclude_filters:
# TODO(noma3ad) use not() operator with Windivert2 after upgrade
_af_filter += f" and (({' or '.join(subnet_exclude_filters)})? false : true)"
proxy_ip, proxy_port = c["proxy_addr"]
# Avoids proxy outbound traffic getting directed to itself
proxy_guard_filter = f"(({af.filter}.DstAddr=={proxy_ip.exploded} and tcp.DstPort=={proxy_port})? false : true)"
_af_filter += f" and {proxy_guard_filter}"
af_filters.append(_af_filter)
if not af_filters:
raise Fatal("At least one ipv4 or ipv6 subnet is expected")
filter = f"{filter} and ({' or '.join(af_filters)})"
debug1(f"[EGRESS] {filter=}")
with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w:
proxy_ipv4, proxy_ipv6 = None, None
if IPFamily.IPv4 in self.network_config:
proxy_ipv4 = self.network_config[IPFamily.IPv4]["proxy_addr"]
proxy_ipv4 = proxy_ipv4[0].exploded, proxy_ipv4[1]
if IPFamily.IPv6 in self.network_config:
proxy_ipv6 = self.network_config[IPFamily.IPv6]["proxy_addr"]
proxy_ipv6 = proxy_ipv6[0].exploded, proxy_ipv6[1]
ready_cb()
verbose = get_verbose_level()
for pkt in w:
verbose >= 3 and debug3("[EGRESS] " + repr_pkt(pkt))
if pkt.tcp.syn and not pkt.tcp.ack:
# SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK)
self.conntrack.add(
socket.IPPROTO_TCP,
pkt.src_addr,
pkt.src_port,
pkt.dst_addr,
pkt.dst_port,
ConnState.TCP_SYN_SENT,
)
if pkt.tcp.fin:
# FIN sent (start of graceful close our side, and we wait for ACK)
self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_WAIT_1)
if pkt.tcp.rst:
# RST sent (initiate abrupt connection teardown from our side, so we don't expect any reply)
self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port)
# DNAT
if pkt.ipv4 and proxy_ipv4:
pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv4
if pkt.ipv6 and proxy_ipv6:
pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv6
# XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well
# by setting src_addr to loopback address.
# Otherwise injecting packet will be ignored by Windows network stack
# as they packet has to cross public to private address space.
# See: https://github.com/basil00/Divert/issues/82
# Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets.
# >>> pkt.dst_addr = proxy_ipv4
w.send(pkt, recalculate_checksum=True)
def _ingress_divert(self, ready_cb):
"""handles incoming packets from proxy"""
proto = IPProtocol.TCP
# Windivert treats all local process traffic as outbound, regardless of origin external/loopback iface
direction = "outbound"
proxy_addr_filters = []
for af, c in self.network_config.items():
if not c["subnets"]:
continue
proxy_ip, proxy_port = c["proxy_addr"]
# "ip.SrcAddr=={hex(int(proxy_ip))}" # only Windivert >=2 supports this
proxy_addr_filters.append(f"{af.filter}.SrcAddr=={proxy_ip.exploded} and tcp.SrcPort=={proxy_port}")
if not proxy_addr_filters:
raise Fatal("At least one ipv4 or ipv6 address is expected")
filter = f"{direction} and {proto.filter} and ({' or '.join(proxy_addr_filters)})"
debug1(f"[INGRESS] {filter=}")
with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w:
ready_cb()
verbose = get_verbose_level()
for pkt in w:
verbose >= 3 and debug3("[INGRESS] " + repr_pkt(pkt))
if pkt.tcp.syn and pkt.tcp.ack:
# SYN+ACK received (connection established from proxy
conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED)
elif pkt.tcp.rst:
# RST received - Abrupt connection teardown initiated by proxy. Don't expect anymore packets
conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port)
# https://wiki.wireshark.org/TCP-4-times-close.md
elif pkt.tcp.fin and pkt.tcp.ack:
# FIN+ACK received (Passive close by proxy. Don't expect any more packets. proxy expects an ACK)
conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port)
elif pkt.tcp.fin:
# FIN received (proxy initiated graceful close. Expect a final ACK for a FIN packet)
conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_CLOSE_WAIT)
else:
# data fragments and ACKs
conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port)
if not conn:
verbose >= 2 and debug2("Unexpected packet: " + repr_pkt(pkt))
continue
pkt.src_addr = conn.dst_addr
pkt.tcp.src_port = conn.dst_port
w.send(pkt, recalculate_checksum=True)
def _connection_gc(self, ready_cb):
ready_cb()
while True:
time.sleep(5)
self.conntrack.gc()

40
sshuttle/namespace.py Normal file
View File

@ -0,0 +1,40 @@
import os
import ctypes
import ctypes.util
from sshuttle.helpers import Fatal, debug1, debug2
CLONE_NEWNET = 0x40000000
NETNS_RUN_DIR = "/var/run/netns"
def enter_namespace(namespace, namespace_pid):
if namespace:
namespace_dir = f'{NETNS_RUN_DIR}/{namespace}'
else:
namespace_dir = f'/proc/{namespace_pid}/ns/net'
if not os.path.exists(namespace_dir):
raise Fatal('The namespace %r does not exists.' % namespace_dir)
debug2('loading libc')
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
default_errcheck = libc.setns.errcheck
def errcheck(ret, *args):
if ret == -1:
e = ctypes.get_errno()
raise Fatal(e, os.strerror(e))
if default_errcheck:
return default_errcheck(ret, *args)
libc.setns.errcheck = errcheck # type: ignore
debug1('Entering namespace %r' % namespace_dir)
with open(namespace_dir) as fd:
libc.setns(fd.fileno(), CLONE_NEWNET)
debug1('Namespace %r successfully set' % namespace_dir)

View File

@ -1,5 +1,6 @@
import re
import socket
import sys
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__
@ -37,9 +38,9 @@ def parse_subnetport_file(s):
def parse_subnetport(s):
if s.count(':') > 1:
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
rx = r'(?:\[?(?:\*\.)?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
else:
rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
rx = r'((?:\*\.)?[\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
m = re.match(rx, s)
if not m:
@ -132,9 +133,19 @@ def parse_ipport(s):
def parse_list(lst):
"""Parse a comma separated string into a list."""
return re.split(r'[\s,]+', lst.strip()) if lst else []
def parse_namespace(namespace):
try:
assert re.fullmatch(
r'(@?[a-z_A-Z]\w+(?:\.@?[a-z_A-Z]\w+)*)', namespace)
return namespace
except AssertionError:
raise Fatal("%r is not a valid namespace name." % namespace)
class Concat(Action):
def __init__(self, option_strings, dest, nargs=None, **kwargs):
if nargs is not None:
@ -146,9 +157,33 @@ class Concat(Action):
setattr(namespace, self.dest, curr_value + values)
parser = ArgumentParser(
# Override one function in the ArgumentParser so that we can have
# better control for how we parse files containing arguments. We
# expect one argument per line, but strip whitespace/quotes from the
# beginning/end of the lines.
class MyArgumentParser(ArgumentParser):
def convert_arg_line_to_args(self, arg_line):
# Ignore comments
if arg_line.startswith("#"):
return []
# strip whitespace at beginning and end of line
arg_line = arg_line.strip()
# When copying parameters from the command line to a file,
# some users might copy the quotes they used on the command
# line into the config file. We ignore these if the line
# starts and ends with the same quote.
if arg_line.startswith("'") and arg_line.endswith("'") or \
arg_line.startswith('"') and arg_line.endswith('"'):
arg_line = arg_line[1:-1]
return [arg_line]
parser = MyArgumentParser(
prog="sshuttle",
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
usage="%(prog)s [-l [ip:]port] -r [user@]sshserver[:port] <subnets...>",
fromfile_prefix_chars="@"
)
parser.add_argument(
@ -196,6 +231,7 @@ parser.add_argument(
type=parse_list,
help="""
capture and forward DNS requests made to the following servers
(comma separated)
"""
)
parser.add_argument(
@ -208,9 +244,14 @@ parser.add_argument(
"""
)
if sys.platform == 'win32':
method_choices = ["auto", "windivert"]
else:
method_choices = ["auto", "nft", "nat", "tproxy", "pf", "ipfw"]
parser.add_argument(
"--method",
choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
choices=method_choices,
metavar="TYPE",
default="auto",
help="""
@ -256,7 +297,7 @@ parser.add_argument(
action="count",
default=0,
help="""
increase debug message verbosity
increase debug message verbosity (can be used more than once)
"""
)
parser.add_argument(
@ -275,6 +316,22 @@ parser.add_argument(
the command to use to connect to the remote [%(default)s]
"""
)
parser.add_argument(
"--no-cmd-delimiter",
action="store_false",
dest="add_cmd_delimiter",
help="""
do not add a double dash before the python command
"""
)
parser.add_argument(
"--remote-shell",
metavar="PROGRAM",
help="""
alternate remote shell program instead of defacto posix shell.
For Windows targets it would be either `cmd` or `powershell` unless something like git-bash is in use.
"""
)
parser.add_argument(
"--seed-hosts",
metavar="HOSTNAME[,HOSTNAME]",
@ -356,6 +413,12 @@ parser.add_argument(
apply all the rules only to this linux user
"""
)
parser.add_argument(
"--group",
help="""
apply all the rules only to this linux group
"""
)
parser.add_argument(
"--firewall",
action="store_true",
@ -370,18 +433,15 @@ parser.add_argument(
(internal use only)
"""
)
parser.add_argument(
"--sudoers",
action="store_true",
help="""
Add sshuttle to the sudoers for this user
"""
)
parser.add_argument(
"--sudoers-no-modify",
action="store_true",
help="""
Prints the sudoers config to STDOUT and DOES NOT modify anything.
Prints a sudo configuration to STDOUT which allows a user to
run sshuttle without a password. This option is INSECURE because,
with some cleverness, it also allows the user to run any command
as root without a password. The output also includes a suggested
method for you to install the configuration.
"""
)
parser.add_argument(
@ -389,16 +449,7 @@ parser.add_argument(
default="",
help="""
Set the user name or group with %%group_name for passwordless operation.
Default is the current user.set ALL for all users. Only works with
--sudoers or --sudoers-no-modify option.
"""
)
parser.add_argument(
"--sudoers-filename",
default="sshuttle_auto",
help="""
Set the file name for the sudoers.d file to be added. Default is
"sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option.
Default is the current user. Only works with the --sudoers-no-modify option.
"""
)
parser.add_argument(
@ -412,8 +463,26 @@ parser.add_argument(
parser.add_argument(
"-t", "--tmark",
metavar="[MARK]",
default="1",
default="0x01",
help="""
transproxy optional traffic mark with provided MARK value
tproxy optional traffic mark with provided MARK value in
hexadecimal (default '0x01')
"""
)
if sys.platform == 'linux':
net_ns_group = parser.add_mutually_exclusive_group(
required=False)
net_ns_group.add_argument(
'--namespace',
type=parse_namespace,
help="Run inside of a net namespace with the given name."
)
net_ns_group.add_argument(
'--namespace-pid',
type=int,
help="""
Run inside the net namespace used by the process with
the given pid."""
)

View File

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

View File

@ -5,7 +5,7 @@ import traceback
import time
import sys
import os
import platform
import io
import sshuttle.ssnet as ssnet
@ -14,7 +14,7 @@ import sshuttle.hostwatch as hostwatch
import subprocess as ssubprocess
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
resolvconf_random_nameserver, which, get_env
get_random_nameserver, which, get_env, SocketRWShim
def _ipmatch(ipstr):
@ -35,7 +35,6 @@ def _ipmatch(ipstr):
elif g[3] is None:
ips += '.0'
width = min(width, 24)
ips = ips
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
@ -80,6 +79,20 @@ def _route_iproute(line):
return ipw, int(mask)
def _route_windows(line):
if " On-link " not in line:
return None, None
dest, net_mask = re.split(r'\s+', line.strip())[:2]
if net_mask == "255.255.255.255":
return None, None
for p in ('127.', '0.', '224.', '169.254.'):
if dest.startswith(p):
return None, None
ipw = _ipmatch(dest)
mask = _maskbits(_ipmatch(net_mask))
return ipw, mask
def _list_routes(argv, extract_route):
# FIXME: IPv4 only
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
@ -96,20 +109,23 @@ def _list_routes(argv, extract_route):
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait()
if rv != 0:
log('WARNING: %r returned %d\n' % (argv, rv))
log('WARNING: %r returned %d' % (argv, rv))
return routes
def list_routes():
if which('ip'):
routes = _list_routes(['ip', 'route'], _route_iproute)
elif which('netstat'):
routes = _list_routes(['netstat', '-rn'], _route_netstat)
if sys.platform == 'win32':
routes = _list_routes(['route', 'PRINT', '-4'], _route_windows)
else:
log('WARNING: Neither "ip" nor "netstat" were found on the server. '
'--auto-nets feature will not work.\n')
routes = []
if which('ip'):
routes = _list_routes(['ip', 'route'], _route_iproute)
elif which('netstat'):
routes = _list_routes(['netstat', '-rn'], _route_netstat)
else:
log('WARNING: Neither "ip" nor "netstat" were found on the server. '
'--auto-nets feature will not work.')
routes = []
for (family, ip, width) in routes:
if not ip.startswith('0.') and not ip.startswith('127.'):
@ -135,7 +151,7 @@ def start_hostwatch(seed_hosts, auto_hosts):
s1.close()
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
except Exception:
log('%s\n' % _exc_dump())
log('%s' % _exc_dump())
rv = 98
finally:
os._exit(rv)
@ -183,7 +199,7 @@ class DnsProxy(Handler):
self.tries += 1
if self.to_nameserver is None:
_, peer = resolvconf_random_nameserver(False)
_, peer = get_random_nameserver()
port = 53
else:
peer = self.to_ns_peer
@ -191,12 +207,11 @@ class DnsProxy(Handler):
family, sockaddr = self._addrinfo(peer, port)
sock = socket.socket(family, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
sock.connect(sockaddr)
self.peers[sock] = peer
debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries))
debug2('DNS: sending to %r:%d (try %d)' % (peer, port, self.tries))
try:
sock.send(self.request)
self.socks.append(sock)
@ -206,11 +221,11 @@ class DnsProxy(Handler):
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS send to %r: %s\n' % (peer, e))
debug2('DNS send to %r: %s' % (peer, e))
self.try_send()
return
else:
log('DNS send to %r: %s\n' % (peer, e))
log('DNS send to %r: %s' % (peer, e))
return
def callback(self, sock):
@ -227,13 +242,13 @@ class DnsProxy(Handler):
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS recv from %r: %s\n' % (peer, e))
debug2('DNS recv from %r: %s' % (peer, e))
self.try_send()
return
else:
log('DNS recv from %r: %s\n' % (peer, e))
log('DNS recv from %r: %s' % (peer, e))
return
debug2('DNS response: %d bytes\n' % len(data))
debug2('DNS response: %d bytes' % len(data))
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
self.ok = False
@ -247,16 +262,14 @@ class UdpProxy(Handler):
self.mux = mux
self.chan = chan
self.sock = sock
if family == socket.AF_INET:
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
def send(self, dstip, data):
debug2(' s: UDP: sending to %r port %d\n' % dstip)
debug2('UDP: sending to %r port %d' % dstip)
try:
self.sock.sendto(data, dstip)
except socket.error:
_, e = sys.exc_info()[:2]
log(' 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
def callback(self, sock):
@ -264,147 +277,166 @@ class UdpProxy(Handler):
data, peer = sock.recvfrom(4096)
except socket.error:
_, 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
debug2(' s: UDP response: %d bytes\n' % len(data))
debug2('UDP response: %d bytes' % len(data))
hdr = b("%s,%r," % (peer[0], peer[1]))
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
def main(latency_control, auto_hosts, to_nameserver, auto_nets):
debug1(' s: Starting server with Python version %s\n'
% platform.python_version())
def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
auto_nets):
try:
helpers.logprefix = ' s: '
helpers.logprefix = ' s: '
debug1('latency control setting = %r\n' % latency_control)
debug1('latency control setting = %r' % latency_control)
if latency_buffer_size:
import sshuttle.ssnet as ssnet
ssnet.LATENCY_BUFFER_SIZE = latency_buffer_size
# synchronization header
sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush()
# synchronization header
sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush()
handlers = []
mux = Mux(sys.stdin, sys.stdout)
handlers.append(mux)
handlers = []
# get unbuffered stdin and stdout in binary mode. Equivalent to stdin.buffer/stdout.buffer (Only available in Python 3)
r, w = io.FileIO(0, mode='r'), io.FileIO(1, mode='w')
if sys.platform == 'win32':
def _deferred_exit():
time.sleep(1) # give enough time to write logs to stderr
os._exit(23)
shim = SocketRWShim(r, w, on_end=_deferred_exit)
mux = Mux(*shim.makefiles())
else:
mux = Mux(r, w)
handlers.append(mux)
debug1('auto-nets:' + str(auto_nets) + '\n')
if auto_nets:
routes = list(list_routes())
debug1('available routes:\n')
debug1('auto-nets:' + str(auto_nets))
if auto_nets:
routes = list(list_routes())
debug1('available routes:')
for r in routes:
debug1(' %d/%s/%d' % r)
else:
routes = []
routepkt = ''
for r in routes:
debug1(' %d/%s/%d\n' % r)
else:
routes = []
routepkt += '%d,%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
routepkt = ''
for r in routes:
routepkt += '%d,%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
hw = Hostwatch()
hw.leftover = b('')
hw = Hostwatch()
hw.leftover = b('')
def hostwatch_ready(sock):
assert(hw.pid)
content = hw.sock.recv(4096)
if content:
lines = (hw.leftover + content).split(b('\n'))
if lines[-1]:
# no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop()
lines.append(b(''))
def hostwatch_ready(sock):
assert hw.pid
content = hw.sock.recv(4096)
if content:
lines = (hw.leftover + content).split(b('\n'))
if lines[-1]:
# no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop()
lines.append(b(''))
else:
hw.leftover = b('')
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
else:
hw.leftover = b('')
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
else:
raise Fatal(' s: hostwatch process died')
raise Fatal('hostwatch process died')
def got_host_req(data):
if not hw.pid:
(hw.pid, hw.sock) = start_hostwatch(
data.decode("ASCII").strip().split(), auto_hosts)
handlers.append(Handler(socks=[hw.sock],
callback=hostwatch_ready))
mux.got_host_req = got_host_req
def got_host_req(data):
if not hw.pid:
(hw.pid, hw.sock) = start_hostwatch(
data.decode("ASCII").strip().split(), auto_hosts)
handlers.append(Handler(socks=[hw.sock],
callback=hostwatch_ready))
mux.got_host_req = got_host_req
def new_channel(channel, data):
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
family = int(family)
# AF_INET is the same constant on Linux and BSD but AF_INET6
# is different. As the client and server can be running on
# different platforms we can not just set the socket family
# to what comes in the wire.
if family != socket.AF_INET:
family = socket.AF_INET6
dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
dnshandlers = {}
def dns_req(channel, data):
debug2('Incoming DNS request channel=%d.\n' % channel)
h = DnsProxy(mux, channel, data, to_nameserver)
handlers.append(h)
dnshandlers[channel] = h
mux.got_dns_req = dns_req
udphandlers = {}
def udp_req(channel, cmd, data):
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
if cmd == ssnet.CMD_UDP_DATA:
(dstip, dstport, data) = data.split(b(','), 2)
def new_channel(channel, data):
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
family = int(family)
# AF_INET is the same constant on Linux and BSD but AF_INET6
# is different. As the client and server can be running on
# different platforms we can not just set the socket family
# to what comes in the wire.
if family != socket.AF_INET:
family = socket.AF_INET6
dstport = int(dstport)
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
h = udphandlers[channel]
h.send((dstip, dstport), data)
elif cmd == ssnet.CMD_UDP_CLOSE:
debug2('is incoming UDP close\n')
h = udphandlers[channel]
h.ok = False
del mux.channels[channel]
outwrap = ssnet.connect_dst(family, dstip, dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
def udp_open(channel, data):
debug2('Incoming UDP open.\n')
family = int(data)
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
if channel in udphandlers:
raise Fatal(' s: UDP connection channel %d already open' % channel)
else:
h = UdpProxy(mux, channel, family)
dnshandlers = {}
def dns_req(channel, data):
debug2('Incoming DNS request channel=%d.' % channel)
h = DnsProxy(mux, channel, data, to_nameserver)
handlers.append(h)
udphandlers[channel] = h
mux.got_udp_open = udp_open
dnshandlers[channel] = h
mux.got_dns_req = dns_req
while mux.ok:
if hw.pid:
assert(hw.pid > 0)
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
udphandlers = {}
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
def udp_req(channel, cmd, data):
debug2('Incoming UDP request channel=%d, cmd=%d' %
(channel, cmd))
if cmd == ssnet.CMD_UDP_DATA:
(dstip, dstport, data) = data.split(b(','), 2)
dstport = int(dstport)
debug2('is incoming UDP data. %r %d.' % (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:
now = time.time()
remove = []
for channel, h in dnshandlers.items():
if h.timeout < now or not h.ok:
debug3('expiring dnsreqs channel=%d\n' % channel)
remove.append(channel)
h.ok = False
for channel in remove:
del dnshandlers[channel]
if udphandlers:
remove = []
for channel, h in udphandlers.items():
if not h.ok:
debug3('expiring UDP channel=%d\n' % channel)
remove.append(channel)
h.ok = False
for channel in remove:
del udphandlers[channel]
def udp_open(channel, data):
debug2('Incoming UDP open.')
family = int(data)
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd,
data)
if channel in udphandlers:
raise Fatal('UDP connection channel %d already open' %
channel)
else:
h = UdpProxy(mux, channel, family)
handlers.append(h)
udphandlers[channel] = h
mux.got_udp_open = udp_open
while mux.ok:
if hw.pid:
assert hw.pid > 0
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x' % 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

@ -12,7 +12,7 @@ import ipaddress
from urllib.parse import urlparse
import sshuttle.helpers as helpers
from sshuttle.helpers import debug2, which, get_path, Fatal
from sshuttle.helpers import debug2, which, get_path, SocketRWShim, Fatal
def get_module_source(name):
@ -56,16 +56,16 @@ def parse_hostport(rhostport):
# Fix #410 bad username error detect
if ":" in username:
# this will even allow for the username to be empty
username, password = username.split(":")
username, password = username.split(":", 1)
if ":" in host:
# 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
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
host = str(ipaddress.ip_address(host))
except ValueError:
@ -84,7 +84,7 @@ def parse_hostport(rhostport):
return username, password, port, host
def connect(ssh_cmd, rhostport, python, stderr, options):
def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell, options):
username, password, port, host = parse_hostport(rhostport)
if username:
rhost = "{}@{}".format(username, host)
@ -103,11 +103,21 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
empackage(z, 'sshuttle.server') +
b"\n")
# If the exec() program calls sys.exit(), it should exit python
# and the sys.exit(98) call won't be reached (so we try to only
# exit that way in the server). However, if the code that we
# exec() simply returns from main, then we will return from
# exec(). If the server's python process dies, it should stop
# executing and also won't reach sys.exit(98).
#
# So, we shouldn't reach sys.exit(98) and we certainly shouldn't
# reach it immediately after trying to start the server.
pyscript = r"""
import sys, os;
verbosity=%d;
sys.stdin = os.fdopen(0, "rb");
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
stdin = os.fdopen(0, 'rb');
exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
sys.exit(98);
""" % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@ -124,23 +134,72 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
portl = ["-p", str(port)]
else:
portl = []
if python:
pycmd = "'%s' -c '%s'" % (python, pyscript)
else:
pycmd = ("P=python3; $P -V 2>%s || P=python; "
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript))
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
if remote_shell == "cmd":
pycmd = '"%s" -c "%s"' % (python or 'python', pyscript)
elif remote_shell == "powershell":
for c in ('\'', ' ', ';', '(', ')', ','):
pyscript = pyscript.replace(c, '`' + c)
pycmd = '%s -c %s' % (python or 'python', pyscript)
else: # posix shell expected
if python:
pycmd = '"%s" -c "%s"' % (python, pyscript)
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.
devnull = '/dev/null'
pycmd = ("P=python3; $P -V 2>%s || P=python; "
"exec \"$P\" -c %s; exit 97") % \
(devnull, quote(pyscript))
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
if password is not None:
os.environ['SSHPASS'] = str(password)
argv = (["sshpass", "-e"] + sshl +
portl +
[rhost, '--', pycmd])
portl + [rhost])
else:
argv = (sshl +
portl +
[rhost, '--', pycmd])
argv = (sshl + portl + [rhost])
if add_cmd_delimiter:
argv += ['--', pycmd]
else:
argv += [pycmd]
# Our which() function searches for programs in get_path()
# directories (which include PATH). This step isn't strictly
@ -152,19 +211,45 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path()))
argv[0] = abs_path
(s1, s2) = socket.socketpair()
if sys.platform != 'win32':
(s1, s2) = socket.socketpair()
pstdin, pstdout = os.dup(s1.fileno()), os.dup(s1.fileno())
def setup():
# runs in the child process
s2.close()
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close()
def preexec_fn():
# runs in the child process
s2.close()
s1.close()
debug2('executing: %r\n' % argv)
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True, stderr=stderr)
os.close(s1a)
os.close(s1b)
s2.sendall(content)
s2.sendall(content2)
return p, s2
def get_server_io():
os.close(pstdin)
os.close(pstdout)
return s2.makefile("rb", buffering=0), s2.makefile("wb", buffering=0)
else:
# In Windows CPython, BSD sockets are not supported as subprocess stdio
# and select.select() used in ssnet.py won't work on Windows pipes.
# So we have to use both socketpair (for select.select) and pipes (for subprocess.Popen) together
# along with reader/writer threads to stream data between them
# NOTE: Their could be a better way. Need to investigate further on this.
# Either to use sockets as stdio for subprocess. Or to use pipes but with a select() alternative
# https://stackoverflow.com/questions/4993119/redirect-io-of-process-to-windows-socket
pstdin = ssubprocess.PIPE
pstdout = ssubprocess.PIPE
preexec_fn = None
def get_server_io():
shim = SocketRWShim(p.stdout, p.stdin, on_end=lambda: p.terminate())
return shim.makefiles()
# See: stackoverflow.com/questions/48671215/howto-workaround-of-close-fds-true-and-redirect-stdout-stderr-on-windows
close_fds = False if sys.platform == 'win32' else True
debug2("executing: %r" % argv)
p = ssubprocess.Popen(argv, stdin=pstdin, stdout=pstdout, preexec_fn=preexec_fn,
close_fds=close_fds, stderr=stderr, bufsize=0)
rfile, wfile = get_server_io()
wfile.write(content)
wfile.write(content2)
return p, rfile, wfile

View File

@ -4,9 +4,8 @@ import socket
import errno
import select
import os
import fcntl
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, set_non_blocking_io
MAX_CHANNEL = 65535
LATENCY_BUFFER_SIZE = 32768
@ -78,12 +77,13 @@ def _fds(socks):
def _nb_clean(func, *args):
try:
return func(*args)
except OSError:
except (OSError, socket.error):
# Note: In python2 socket.error != OSError (In python3, they are same)
_, e = sys.exc_info()[:2]
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
raise
else:
debug3('%s: err was: %s\n' % (func.__name__, e))
debug3('%s: err was: %s' % (func.__name__, e))
return None
@ -111,7 +111,7 @@ class SockWrapper:
def __init__(self, rsock, wsock, connect_to=None, peername=None):
global _swcount
_swcount += 1
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
debug3('creating new SockWrapper (%d now exist)' % _swcount)
self.exc = None
self.rsock = rsock
self.wsock = wsock
@ -124,9 +124,9 @@ class SockWrapper:
def __del__(self):
global _swcount
_swcount -= 1
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
debug1('%r: deleting (%d remain)' % (self, _swcount))
if self.exc:
debug1('%r: error was: %s\n' % (self, self.exc))
debug1('%r: error was: %s' % (self, self.exc))
def __repr__(self):
if self.rsock == self.wsock:
@ -148,14 +148,14 @@ class SockWrapper:
if not self.connect_to:
return # already connected
self.rsock.setblocking(False)
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
debug3('%r: trying connect to %r' % (self, self.connect_to))
try:
self.rsock.connect(self.connect_to)
# connected successfully (Linux)
self.connect_to = None
except socket.error:
_, e = sys.exc_info()[:2]
debug3('%r: connect result: %s\n' % (self, e))
debug3('%r: connect result: %s' % (self, e))
if e.args[0] == errno.EINVAL:
# this is what happens when you call connect() on a socket
# that is now connected but returned EINPROGRESS last time,
@ -165,22 +165,28 @@ class SockWrapper:
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
socket.SO_ERROR)
e = socket.error(realerr, os.strerror(realerr))
debug3('%r: fixed connect result: %s\n' % (self, e))
debug3('%r: fixed connect result: %s' % (self, e))
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet
elif sys.platform == 'win32' and e.args[0] == errno.WSAEWOULDBLOCK: # 10035
pass # not connected yet
elif e.args[0] == 0:
# connected successfully (weird Linux bug?)
# Sometimes Linux seems to return EINVAL when it isn't
# invalid. This *may* be caused by a race condition
# between connect() and getsockopt(SO_ERROR) (ie. it
# finishes connecting in between the two, so there is no
# longer an error). However, I'm not sure of that.
#
# I did get at least one report that the problem went away
# when we added this, however.
self.connect_to = None
if sys.platform == 'win32':
# On Windows "real" error of EINVAL could be 0, when socket is in connecting state
pass
else:
# connected successfully (weird Linux bug?)
# Sometimes Linux seems to return EINVAL when it isn't
# invalid. This *may* be caused by a race condition
# between connect() and getsockopt(SO_ERROR) (ie. it
# finishes connecting in between the two, so there is no
# longer an error). However, I'm not sure of that.
#
# I did get at least one report that the problem went away
# when we added this, however.
self.connect_to = None
elif e.args[0] == errno.EISCONN:
# connected successfully (BSD)
# connected successfully (BSD + Windows)
self.connect_to = None
elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]:
# a "normal" kind of error
@ -191,13 +197,12 @@ class SockWrapper:
def noread(self):
if not self.shut_read:
debug2('%r: done reading\n' % self)
debug2('%r: done reading' % self)
self.shut_read = True
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
def nowrite(self):
if not self.shut_write:
debug2('%r: done writing\n' % self)
debug2('%r: done writing' % self)
self.shut_write = True
try:
self.wsock.shutdown(SHUT_WR)
@ -214,11 +219,11 @@ class SockWrapper:
return 0 # still connecting
self.wsock.setblocking(False)
try:
return _nb_clean(os.write, self.wsock.fileno(), buf)
return _nb_clean(self.wsock.send, buf)
except OSError:
_, e = sys.exc_info()[:2]
if e.errno == errno.EPIPE:
debug1('%r: uwrite: got EPIPE\n' % self)
debug1('%r: uwrite: got EPIPE' % self)
self.nowrite()
return 0
else:
@ -227,7 +232,7 @@ class SockWrapper:
return 0
def write(self, buf):
assert(buf)
assert buf
return self.uwrite(buf)
def uread(self):
@ -237,7 +242,7 @@ class SockWrapper:
return
self.rsock.setblocking(False)
try:
return _nb_clean(os.read, self.rsock.fileno(), 65536)
return _nb_clean(self.rsock.recv, 65536)
except OSError:
_, e = sys.exc_info()[:2]
self.seterr('uread: %s' % e)
@ -275,12 +280,12 @@ class Handler:
_add(r, i)
def callback(self, sock):
log('--no callback defined-- %r\n' % self)
log('--no callback defined-- %r' % self)
(r, _, _) = select.select(self.socks, [], [], 0)
for s in r:
v = s.recv(4096)
if not v:
log('--closed-- %r\n' % self)
log('--closed-- %r' % self)
self.socks = []
self.ok = False
@ -373,11 +378,6 @@ class Mux(Handler):
if not self.too_full:
self.send(0, CMD_PING, b('rttest'))
self.too_full = True
# ob = []
# for b in self.outbuf:
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
# ob.append(c)
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
def send(self, channel, cmd, data):
assert isinstance(data, bytes)
@ -385,32 +385,34 @@ class Mux(Handler):
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
+ data
self.outbuf.append(p)
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
% (channel, cmd_to_name.get(cmd, hex(cmd)),
len(data), self.fullness))
# debug3('>>> data: %r' % data)
self.fullness += len(data)
def got_packet(self, channel, cmd, data):
debug2('< channel=%d cmd=%s len=%d\n'
debug2('< channel=%d cmd=%s len=%d'
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
# debug3('<<< data: %r' % data)
if cmd == CMD_PING:
self.send(0, CMD_PONG, data)
elif cmd == CMD_PONG:
debug2('received PING response\n')
debug2('received PING response')
self.too_full = False
self.fullness = 0
elif cmd == CMD_EXIT:
self.ok = False
elif cmd == CMD_TCP_CONNECT:
assert(not self.channels.get(channel))
assert not self.channels.get(channel)
if self.new_channel:
self.new_channel(channel, data)
elif cmd == CMD_DNS_REQ:
assert(not self.channels.get(channel))
assert not self.channels.get(channel)
if self.got_dns_req:
self.got_dns_req(channel, data)
elif cmd == CMD_UDP_OPEN:
assert(not self.channels.get(channel))
assert not self.channels.get(channel)
if self.got_udp_open:
self.got_udp_open(channel, data)
elif cmd == CMD_ROUTES:
@ -431,41 +433,33 @@ class Mux(Handler):
else:
callback = self.channels.get(channel)
if not callback:
log('warning: closed channel %d got cmd=%s len=%d\n'
log('warning: closed channel %d got cmd=%s len=%d'
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
else:
callback(cmd, data)
def flush(self):
try:
os.set_blocking(self.wfile.fileno(), False)
except AttributeError:
# python < 3.5
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL)
flags |= os.O_NONBLOCK
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
set_non_blocking_io(self.wfile.fileno())
if self.outbuf and 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])))
wrote = _nb_clean(self.wfile.write, self.outbuf[0])
# self.wfile.flush()
debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0])))
if wrote:
self.outbuf[0] = self.outbuf[0][wrote:]
while self.outbuf and not self.outbuf[0]:
self.outbuf[0:1] = []
def fill(self):
set_non_blocking_io(self.rfile.fileno())
try:
os.set_blocking(self.rfile.fileno(), False)
except AttributeError:
# python < 3.5
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL)
flags |= os.O_NONBLOCK
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
try:
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
# If LATENCY_BUFFER_SIZE is inappropriately large, we will
# get a MemoryError here. Read no more than 1MiB.
read = _nb_clean(self.rfile.read, min(1048576, LATENCY_BUFFER_SIZE))
debug2('mux read: %r' % len(read))
except OSError:
_, e = sys.exc_info()[:2]
raise Fatal('other end: %r' % e)
# log('<<< %r\n' % b)
# log('<<< %r' % b)
if read == b(''): # EOF
self.ok = False
if read:
@ -473,14 +467,12 @@ class Mux(Handler):
def handle(self):
self.fill()
# log('inbuf is: (%d,%d) %r\n'
# % (self.want, len(self.inbuf), self.inbuf))
while 1:
if len(self.inbuf) >= (self.want or HDR_LEN):
(s1, s2, channel, cmd, datalen) = \
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
assert(s1 == b('S'))
assert(s2 == b('S'))
assert s1 == b('S')
assert s2 == b('S')
self.want = datalen + HDR_LEN
if self.want and len(self.inbuf) >= self.want:
data = self.inbuf[HDR_LEN:self.want]
@ -511,7 +503,7 @@ class MuxWrapper(SockWrapper):
self.channel = channel
self.mux.channels[channel] = self.got_packet
self.socks = []
debug2('new channel: %d\n' % channel)
debug2('new channel: %d' % channel)
def __del__(self):
self.nowrite()
@ -527,7 +519,7 @@ class MuxWrapper(SockWrapper):
def setnoread(self):
if not self.shut_read:
debug2('%r: done reading\n' % self)
debug2('%r: done reading' % self)
self.shut_read = True
self.maybe_close()
@ -538,13 +530,13 @@ class MuxWrapper(SockWrapper):
def setnowrite(self):
if not self.shut_write:
debug2('%r: done writing\n' % self)
debug2('%r: done writing' % self)
self.shut_write = True
self.maybe_close()
def maybe_close(self):
if self.shut_read and self.shut_write:
debug2('%r: closing connection\n' % self)
debug2('%r: closing connection' % self)
# remove the mux's reference to us. The python garbage collector
# will then be able to reap our object.
self.mux.channels[self.channel] = None
@ -581,9 +573,9 @@ class MuxWrapper(SockWrapper):
def connect_dst(family, ip, port):
debug2('Connecting to %s:%d\n' % (ip, port))
debug2('Connecting to %s:%d' % (ip, port))
outsock = socket.socket(family)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
return SockWrapper(outsock, outsock,
connect_to=(ip, port),
peername='%s:%d' % (ip, port))
@ -599,11 +591,11 @@ def runonce(handlers, mux):
for s in handlers:
s.pre_select(r, w, x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)'
% (len(handlers), _fds(r), _fds(w), _fds(x),
mux.fullness, mux.too_full))
(r, w, x) = select.select(r, w, x)
debug2(' Ready: %d r=%r w=%r x=%r\n'
debug2(' Ready: %d r=%r w=%r x=%r'
% (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r + w + x
did = {}

View File

@ -10,7 +10,7 @@ def start_syslog():
global _p
with open(os.devnull, 'w') as devnull:
_p = ssubprocess.Popen(
['logger', '-p', 'daemon.notice', '-t', 'sshuttle'],
['logger', '-p', 'daemon.err', '-t', 'sshuttle'],
stdin=ssubprocess.PIPE,
stdout=devnull,
stderr=devnull

View File

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

View File

@ -2,63 +2,44 @@ import os
import sys
import getpass
from uuid import uuid4
from subprocess import Popen, PIPE
from sshuttle.helpers import log, debug1
from distutils import spawn
path_to_sshuttle = sys.argv[0]
path_to_dist_packages = os.path.dirname(os.path.abspath(__file__))[:-9]
# randomize command alias to avoid collisions
command_alias = 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()}
# Template for the sudoers file
template = '''
Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
%(user_name)s ALL=NOPASSWD: %(ca)s
'''
def build_config(user_name):
content = template % {
'ca': command_alias,
'dist_packages': path_to_dist_packages,
'py': sys.executable,
'path': path_to_sshuttle,
'user_name': user_name,
}
"""Generates a sudoers configuration to allow passwordless execution of sshuttle."""
return content
argv0 = os.path.abspath(sys.argv[0])
is_python_script = argv0.endswith('.py')
executable = f"{sys.executable} {argv0}" if is_python_script else argv0
dist_packages = os.path.dirname(os.path.abspath(__file__))
cmd_alias = f"SSHUTTLE{uuid4().hex[-3:].upper()}"
template = f"""
# WARNING: If you intend to restrict a user to only running the
# sshuttle command as root, THIS CONFIGURATION IS INSECURE.
# When a user can run sshuttle as root (with or without a password),
# they can also run other commands as root because sshuttle itself
# can run a command specified by the user with the --ssh-cmd option.
# INSTRUCTIONS: Add this text to your sudo configuration to run
# sshuttle without needing to enter a sudo password. To use this
# configuration, run 'visudo /etc/sudoers.d/sshuttle_auto' as root and
# paste this text into the editor that it opens. If you want to give
# multiple users these privileges, you may wish to use different
# filenames for each one (i.e., /etc/sudoers.d/sshuttle_auto_john).
# This configuration was initially generated by the
# 'sshuttle --sudoers-no-modify' command.
Cmnd_Alias {cmd_alias} = /usr/bin/env PYTHONPATH={dist_packages} {executable} *
{user_name} ALL=NOPASSWD: {cmd_alias}
"""
return template
def save_config(content, file_name):
process = Popen([
'/usr/bin/sudo',
spawn.find_executable('sudoers-add'),
file_name,
], stdout=PIPE, stdin=PIPE)
process.stdin.write(content.encode())
streamdata = process.communicate()[0]
returncode = process.returncode
if returncode:
log('Failed updating sudoers file.\n')
debug1(streamdata)
exit(returncode)
else:
log('Success, sudoers file update.\n')
exit(0)
def sudoers(user_name=None, no_modify=None, file_name=None):
def sudoers(user_name=None):
user_name = user_name or getpass.getuser()
content = build_config(user_name)
if no_modify:
sys.stdout.write(content)
exit(0)
else:
save_config(content, file_name)
sys.stdout.write(content)
exit(0)

View File

@ -1,12 +1,16 @@
import io
import os
from socket import AF_INET, AF_INET6
from mock import Mock, patch, call
from unittest.mock import Mock, patch, call
import pytest
import sshuttle.firewall
def setup_daemon():
stdin = io.StringIO(u"""ROUTES
stdin = io.BytesIO(u"""ROUTES
{inet},24,0,1.2.3.0,8000,9000
{inet},32,1,1.2.3.66,8080,8080
{inet6},64,0,2404:6800:4004:80c::,0,0
@ -15,9 +19,9 @@ NSLIST
{inet},1.2.3.33
{inet6},2404:6800:4004:80c::33
PORTS 1024,1025,1026,1027
GO 1 -
GO 1 - - 0x01 12345
HOST 1.2.3.3,existing
""".format(inet=AF_INET, inet6=AF_INET6))
""".format(inet=AF_INET, inet6=AF_INET6).encode('ASCII'))
stdout = Mock()
return stdin, stdout
@ -59,6 +63,21 @@ def test_rewrite_etc_hosts(tmpdir):
assert orig_hosts.computehash() == new_hosts.computehash()
@patch('os.link')
@patch('os.rename')
def test_rewrite_etc_hosts_no_overwrite(mock_link, mock_rename, tmpdir):
mock_link.side_effect = OSError
mock_rename.side_effect = OSError
with pytest.raises(OSError):
os.link('/test_from', '/test_to')
with pytest.raises(OSError):
os.rename('/test_from', '/test_to')
test_rewrite_etc_hosts(tmpdir)
def test_subnet_weight():
subnets = [
(AF_INET, 16, 0, '192.168.0.0', 0, 0),
@ -108,9 +127,9 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
]
assert stdout.mock_calls == [
call.write('READY test\n'),
call.write(b'READY test\n'),
call.flush(),
call.write('STARTED\n'),
call.write(b'STARTED\n'),
call.flush()
]
assert mock_setup_daemon.mock_calls == [call()]
@ -123,17 +142,22 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
[(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)],
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True,
None),
None,
None,
'0x01'),
call().setup_firewall(
1025, 1027,
[(AF_INET, u'1.2.3.33')],
AF_INET,
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
True,
None),
call().restore_firewall(1024, AF_INET6, True, None),
call().restore_firewall(1025, AF_INET, True, None),
None,
None,
'0x01'),
call().wait_for_firewall_ready(12345),
call().restore_firewall(1024, AF_INET6, True, None, None),
call().restore_firewall(1025, AF_INET, True, None, None),
]

View File

@ -3,7 +3,7 @@ import socket
from socket import AF_INET, AF_INET6
import errno
from mock import patch, call
from unittest.mock import patch, call
import sshuttle.helpers
@ -24,19 +24,19 @@ def test_log(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\n'),
call.flush(),
call.write('prefix: abc'),
call.write('prefix: abc\n'),
call.flush(),
call.write('prefix: message 1\n'),
call.flush(),
call.write('prefix: message 2\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.write(' line2\n'),
call.write(' line3\n'),
call.flush(),
call.write('prefix: message 3\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.write(' line2\n'),
call.write(' line3\n'),
call.flush(),
]
@ -51,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\n'),
call.flush(),
]
@ -76,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\n'),
call.flush(),
]
@ -101,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout):
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
call.write('prefix: message\n'),
call.flush(),
]
@ -143,7 +143,7 @@ nameserver 2404:6800:4004:80c::4
@patch('sshuttle.helpers.open', create=True)
def test_resolvconf_random_nameserver(mock_open):
def test_get_random_nameserver(mock_open):
mock_open.return_value = io.StringIO(u"""
# Generated by NetworkManager
search pri
@ -156,7 +156,7 @@ nameserver 2404:6800:4004:80c::2
nameserver 2404:6800:4004:80c::3
nameserver 2404:6800:4004:80c::4
""")
ns = sshuttle.helpers.resolvconf_random_nameserver(False)
ns = sshuttle.helpers.get_random_nameserver()
assert ns in [
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
@ -192,5 +192,4 @@ def test_family_ip_tuple():
def test_family_to_string():
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
expected = 'AddressFamily.AF_UNIX'
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected
assert isinstance(sshuttle.helpers.family_to_string(socket.AF_UNIX), str)

View File

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

View File

@ -2,7 +2,7 @@ import socket
from socket import AF_INET, AF_INET6
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.helpers import Fatal, get_env
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
@ -92,7 +92,7 @@ def test_assert_features():
@patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf')
assert not method.firewall_command("somthing")
assert not method.firewall_command("something")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
AF_INET, socket.IPPROTO_TCP,
@ -115,7 +115,7 @@ def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
@patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf')
assert not method.firewall_command("somthing")
assert not method.firewall_command("something")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
AF_INET, socket.IPPROTO_TCP,
@ -138,7 +138,7 @@ def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
@patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf')
assert not method.firewall_command("somthing")
assert not method.firewall_command("something")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
AF_INET, socket.IPPROTO_TCP,
@ -186,7 +186,9 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False,
None)
None,
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -225,7 +227,9 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -238,7 +242,9 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
None,
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -267,7 +273,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, AF_INET, False, None)
method.restore_firewall(1025, AF_INET, False, None, None)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle-1025 -F all'),
@ -298,7 +304,9 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False,
None)
None,
None,
'0x01')
assert mock_pfctl.mock_calls == [
call('-s all'),
@ -330,7 +338,9 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -343,7 +353,9 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
None,
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
@ -370,8 +382,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, AF_INET, False, None)
method.restore_firewall(1024, AF_INET6, False, None)
method.restore_firewall(1025, AF_INET, False, None, None)
method.restore_firewall(1024, AF_INET6, False, None, None)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle-1025 -F all'),
@ -401,11 +413,13 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False,
None)
None,
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd50441a, ANY),
call(mock_pf_get_dev(), 0xcd50441a, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
@ -437,7 +451,9 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
None,
'0x01')
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
@ -450,10 +466,12 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
None,
None,
'0x01')
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd60441a, ANY),
call(mock_pf_get_dev(), 0xcd50441a, ANY),
call(mock_pf_get_dev(), 0xcd50441a, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
@ -475,8 +493,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, AF_INET, False, None)
method.restore_firewall(1024, AF_INET6, False, None)
method.restore_firewall(1025, AF_INET, False, None, None)
method.restore_firewall(1024, AF_INET6, False, None, None)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle-1025 -F all'),

View File

@ -1,13 +1,12 @@
import socket
from socket import AF_INET, AF_INET6
from mock import Mock, patch, call
from unittest.mock import Mock, patch, call
from sshuttle.methods import get_method
@patch("sshuttle.methods.tproxy.recvmsg")
def test_get_supported_features_recvmsg(mock_recvmsg):
def test_get_supported_features():
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
@ -15,15 +14,6 @@ def test_get_supported_features_recvmsg(mock_recvmsg):
assert features.dns
@patch("sshuttle.methods.tproxy.recvmsg", None)
def test_get_supported_features_norecvmsg():
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
assert not features.udp
assert not features.dns
def test_get_tcp_dstip():
sock = Mock()
sock.getsockname.return_value = ('127.0.0.1', 1024)
@ -88,13 +78,12 @@ def test_assert_features():
def test_firewall_command():
method = get_method('tproxy')
assert not method.firewall_command("somthing")
assert not method.firewall_command("something")
@patch('sshuttle.methods.tproxy.ipt')
@patch('sshuttle.methods.tproxy.ipt_ttl')
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
mock_ipt_chain_exists.return_value = True
method = get_method('tproxy')
assert method.name == 'tproxy'
@ -108,13 +97,14 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True,
None)
None,
None,
'0x01')
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', 'sshuttle-d-1024')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
@ -133,20 +123,24 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x01',
'--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
'--set-mark', '1'),
'--set-mark', '0x01'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
@ -160,31 +154,31 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'--tproxy-mark', '0x01', '--dest',
u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
'--on-port', '1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'--tproxy-mark', '0x01', '--dest',
u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
'--on-port', '1024')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
method.restore_firewall(1025, AF_INET6, True, None)
method.restore_firewall(1025, AF_INET6, True, None, None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET6, 'mangle', 'sshuttle-m-1025'),
call(AF_INET6, 'mangle', 'sshuttle-t-1025'),
call(AF_INET6, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'),
@ -196,7 +190,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
# IPV4
@ -208,13 +201,14 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
None,
None,
'0x01')
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
@ -233,19 +227,23 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '0x01', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x01', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
'-j', 'MARK', '--set-mark', '1'),
'-j', 'MARK', '--set-mark', '0x01'),
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
'--dport', '80:80'),
@ -259,29 +257,27 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
'--dport', '80:80'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24',
'--set-mark', '0x01', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24',
'--set-mark', '0x01', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp'),
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
method.restore_firewall(1025, AF_INET, True, None)
method.restore_firewall(1025, AF_INET, True, None, None)
assert mock_ipt_chain_exists.mock_calls == [
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', 'sshuttle-d-1025')
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
@ -293,5 +289,4 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()

View File

@ -1,5 +1,6 @@
import socket
from argparse import ArgumentTypeError as Fatal
from unittest.mock import patch
import pytest
@ -27,6 +28,23 @@ _ip6_reprs = {
_ip6_swidths = (48, 64, 96, 115, 128)
def _mock_getaddrinfo(host, *_):
return {
"example.com": [
(socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('2606:2800:220:1:248:1893:25c8:1946', 0, 0, 0)),
(socket.AF_INET, socket.SOCK_STREAM, 0, '', ('93.184.216.34', 0)),
],
"my.local": [
(socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('::1', 0, 0, 0)),
(socket.AF_INET, socket.SOCK_STREAM, 0, '', ('127.0.0.1', 0)),
],
"*.blogspot.com": [
(socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('2404:6800:4004:821::2001', 0, 0, 0)),
(socket.AF_INET, socket.SOCK_STREAM, 0, '', ('142.251.42.129', 0)),
],
}.get(host, [])
def test_parse_subnetport_ip4():
for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr) \
@ -100,3 +118,91 @@ def test_parse_subnetport_ip6_with_mask_and_port():
== [(socket.AF_INET6, ip, 128, 80, 80)]
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/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") == []
@patch('sshuttle.options.socket.getaddrinfo', side_effect=_mock_getaddrinfo)
def test_parse_subnetport_host(mock_getaddrinfo):
assert set(sshuttle.options.parse_subnetport('example.com')) \
== set([
(socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 0, 0),
(socket.AF_INET, '93.184.216.34', 32, 0, 0),
])
assert set(sshuttle.options.parse_subnetport('my.local')) \
== set([
(socket.AF_INET6, '::1', 128, 0, 0),
(socket.AF_INET, '127.0.0.1', 32, 0, 0),
])
assert set(sshuttle.options.parse_subnetport('*.blogspot.com')) \
== set([
(socket.AF_INET6, '2404:6800:4004:821::2001', 128, 0, 0),
(socket.AF_INET, '142.251.42.129', 32, 0, 0),
])
@patch('sshuttle.options.socket.getaddrinfo', side_effect=_mock_getaddrinfo)
def test_parse_subnetport_host_with_port(mock_getaddrinfo):
assert set(sshuttle.options.parse_subnetport('example.com:80')) \
== set([
(socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 80, 80),
(socket.AF_INET, '93.184.216.34', 32, 80, 80),
])
assert set(sshuttle.options.parse_subnetport('example.com:80-90')) \
== set([
(socket.AF_INET6, '2606:2800:220:1:248:1893:25c8:1946', 128, 80, 90),
(socket.AF_INET, '93.184.216.34', 32, 80, 90),
])
assert set(sshuttle.options.parse_subnetport('my.local:445')) \
== set([
(socket.AF_INET6, '::1', 128, 445, 445),
(socket.AF_INET, '127.0.0.1', 32, 445, 445),
])
assert set(sshuttle.options.parse_subnetport('my.local:445-450')) \
== set([
(socket.AF_INET6, '::1', 128, 445, 450),
(socket.AF_INET, '127.0.0.1', 32, 445, 450),
])
assert set(sshuttle.options.parse_subnetport('*.blogspot.com:80')) \
== set([
(socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 80),
(socket.AF_INET, '142.251.42.129', 32, 80, 80),
])
assert set(sshuttle.options.parse_subnetport('*.blogspot.com:80-90')) \
== set([
(socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 90),
(socket.AF_INET, '142.251.42.129', 32, 80, 90),
])
def test_parse_namespace():
valid_namespaces = [
'my_namespace',
'my.namespace',
'my_namespace_with_underscore',
'MyNamespace',
'@my_namespace',
'my.long_namespace.with.multiple.dots',
'@my.long_namespace.with.multiple.dots',
'my.Namespace.With.Mixed.Case',
]
for namespace in valid_namespaces:
assert sshuttle.options.parse_namespace(namespace) == namespace
invalid_namespaces = [
'',
'123namespace',
'my-namespace',
'my_namespace!',
'.my_namespace',
'my_namespace.',
'my..namespace',
]
for namespace in invalid_namespaces:
with pytest.raises(Fatal, match="'.*' is not a valid namespace name."):
sshuttle.options.parse_namespace(namespace)

View File

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

View File

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

10
tox.ini
View File

@ -1,18 +1,16 @@
[tox]
downloadcache = {toxworkdir}/cache/
envlist =
py35,
py36,
py37,
py38,
py39,
py310,
[testenv]
basepython =
py36: python3.6
py37: python3.7
py38: python3.8
py39: python3.9
py310: python3.10
py311: python3.11
py312: python3.12
commands =
pip install -e .
# actual flake8 test

1425
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff