mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-06-24 20:01:31 +02:00
Compare commits
No commits in common. "master" and "v1.0.1" have entirely different histories.
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
@ -1,13 +0,0 @@
|
|||||||
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
70
.github/workflows/codeql.yml
vendored
@ -1,70 +0,0 @@
|
|||||||
# 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
|
|
29
.github/workflows/pythonpackage.yml
vendored
29
.github/workflows/pythonpackage.yml
vendored
@ -8,7 +8,6 @@ on:
|
|||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
workflow_dispatch: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -16,23 +15,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||||
poetry-version: ["main"]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install uv
|
- name: Install dependencies
|
||||||
uses: astral-sh/setup-uv@v6
|
run: |
|
||||||
with:
|
python -m pip install --upgrade pip
|
||||||
version: "0.4.30"
|
pip install -r requirements-tests.txt
|
||||||
enable-cache: true
|
|
||||||
cache-dependency-glob: "uv.lock"
|
|
||||||
- name: Install the project
|
|
||||||
run: uv sync --all-extras --dev
|
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: uv run flake8 sshuttle tests --count --show-source --statistics
|
run: |
|
||||||
- name: Run the automated tests
|
flake8 sshuttle tests --count --show-source --statistics
|
||||||
run: uv run pytest -v
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
PYTHONPATH=$PWD pytest
|
||||||
|
66
.github/workflows/release-please.yml
vendored
66
.github/workflows/release-please.yml
vendored
@ -1,66 +0,0 @@
|
|||||||
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
|
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
|
/sshuttle/version.py
|
||||||
/tmp/
|
/tmp/
|
||||||
/.coverage
|
|
||||||
/.cache/
|
/.cache/
|
||||||
/.eggs/
|
/.eggs/
|
||||||
/.tox/
|
/.tox/
|
||||||
@ -15,6 +15,3 @@
|
|||||||
/.redo
|
/.redo
|
||||||
/.pytest_cache/
|
/.pytest_cache/
|
||||||
/.python-version
|
/.python-version
|
||||||
/.direnv/
|
|
||||||
/result
|
|
||||||
/.vscode/
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
python 3.10.6
|
|
54
CHANGELOG.md
54
CHANGELOG.md
@ -1,54 +0,0 @@
|
|||||||
# 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 >= 3.10 for docs ([bf29464](https://github.com/sshuttle/sshuttle/commit/bf294643e283cef9fb285d44e307e958686caf46))
|
|
77
CHANGES.rst
77
CHANGES.rst
@ -1,77 +1,12 @@
|
|||||||
==========
|
==========
|
||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
Release notes now moved to https://github.com/sshuttle/sshuttle/releases/
|
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`_.
|
||||||
|
|
||||||
These are the old release notes.
|
.. _`Keep a Changelog`: http://keepachangelog.com/
|
||||||
|
.. _`Semantic Versioning`: http://semver.org/
|
||||||
|
|
||||||
1.0.5 - 2020-12-29
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Added
|
|
||||||
~~~~~
|
|
||||||
* IPv6 support in nft method.
|
|
||||||
* Intercept DNS requests sent by systemd-resolved.
|
|
||||||
* Set default tmark.
|
|
||||||
* Fix python2 server compatibility.
|
|
||||||
* Python 3.9 support.
|
|
||||||
|
|
||||||
Fixed
|
|
||||||
~~~~~
|
|
||||||
* Change license text to LGPL-2.1
|
|
||||||
* Fix #494 sshuttle caught in infinite select() loop.
|
|
||||||
* Include sshuttle version in verbose output.
|
|
||||||
* Add psutil as dependency in setup.py
|
|
||||||
* When subnets and excludes are specified with hostnames, use all IPs.
|
|
||||||
* Update/document client's handling of IPv4 and IPv6.
|
|
||||||
* Update sdnotify.py documentation.
|
|
||||||
* Allow no remote to work.
|
|
||||||
* Make prefixes in verbose output more consistent.
|
|
||||||
* Make nat and nft rules consistent; improve rule ordering.
|
|
||||||
* Make server and client handle resolv.conf differently.
|
|
||||||
* Fix handling OSError in FirewallClient#__init__
|
|
||||||
* Refactor automatic method selection.
|
|
||||||
|
|
||||||
Removed
|
|
||||||
~~~~~~~
|
|
||||||
* Drop testing of Python 3.5
|
|
||||||
|
|
||||||
|
|
||||||
1.0.4 - 2020-08-24
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Fixed
|
|
||||||
~~~~~
|
|
||||||
* Allow Mux() flush/fill to work with python < 3.5
|
|
||||||
* Fix parse_hostport to always return string for host.
|
|
||||||
* Require -r/--remote parameter.
|
|
||||||
* Add missing package in OpenWRT documentation.
|
|
||||||
* Fix doc about --listen option.
|
|
||||||
* README: add Ubuntu.
|
|
||||||
* Increase IP4 ttl to 63 hops instead of 42.
|
|
||||||
* Fix formatting in installation.rst
|
|
||||||
|
|
||||||
|
|
||||||
1.0.3 - 2020-07-12
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Fixed
|
|
||||||
~~~~~
|
|
||||||
* Ask setuptools to require Python 3.5 and above.
|
|
||||||
* Add missing import.
|
|
||||||
* Fix formatting typos in usage docs
|
|
||||||
|
|
||||||
|
|
||||||
1.0.2 - 2020-06-18
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Fixed
|
|
||||||
~~~~~
|
|
||||||
* Leave use of default port to ssh command.
|
|
||||||
* Remove unwanted references to Python 2.7 in docs.
|
|
||||||
* Replace usage of deprecated imp.
|
|
||||||
* Fix connection with @ sign in username.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.1 - 2020-06-05
|
1.0.1 - 2020-06-05
|
||||||
@ -130,7 +65,7 @@ Fixed
|
|||||||
|
|
||||||
Added
|
Added
|
||||||
~~~~~
|
~~~~~
|
||||||
* doas support as replacement for sudo on OpenBSD.
|
* doas support as replacmeent for sudo on OpenBSD.
|
||||||
* Added ChromeOS section to documentation (#262)
|
* Added ChromeOS section to documentation (#262)
|
||||||
* Add --no-sudo-pythonpath option
|
* Add --no-sudo-pythonpath option
|
||||||
|
|
||||||
|
193
LICENSE
193
LICENSE
@ -1,14 +1,13 @@
|
|||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||||
Version 2.1, February 1999
|
Version 2, June 1991
|
||||||
|
|
||||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
Copyright (C) 1991 Free Software Foundation, Inc.
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
[This is the first released version of the Lesser GPL. It also counts
|
[This is the first released version of the library GPL. It is
|
||||||
as the successor of the GNU Library Public License, version 2, hence
|
numbered 2 because it goes with version 2 of the ordinary GPL.]
|
||||||
the version number 2.1.]
|
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
@ -17,109 +16,97 @@ freedom to share and change it. By contrast, the GNU General Public
|
|||||||
Licenses are intended to guarantee your freedom to share and change
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
free software--to make sure the software is free for all its users.
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
This license, the Lesser General Public License, applies to some
|
This license, the Library General Public License, applies to some
|
||||||
specially designated software packages--typically libraries--of the
|
specially designated Free Software Foundation software, and to any
|
||||||
Free Software Foundation and other authors who decide to use it. You
|
other libraries whose authors decide to use it. You can use it for
|
||||||
can use it too, but we suggest you first think carefully about whether
|
your libraries, too.
|
||||||
this license or the ordinary General Public License is the better
|
|
||||||
strategy to use in any particular case, based on the explanations below.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom of use,
|
When we speak of free software, we are referring to freedom, not
|
||||||
not price. Our General Public Licenses are designed to make sure that
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
you have the freedom to distribute copies of free software (and charge
|
have the freedom to distribute copies of free software (and charge for
|
||||||
for this service if you wish); that you receive source code or can get
|
this service if you wish), that you receive source code or can get it
|
||||||
it if you want it; that you can change the software and use pieces of
|
if you want it, that you can change the software or use pieces of it
|
||||||
it in new free programs; and that you are informed that you can do
|
in new free programs; and that you know you can do these things.
|
||||||
these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
To protect your rights, we need to make restrictions that forbid
|
||||||
distributors to deny you these rights or to ask you to surrender these
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
rights. These restrictions translate to certain responsibilities for
|
These restrictions translate to certain responsibilities for you if
|
||||||
you if you distribute copies of the library or if you modify it.
|
you distribute copies of the library, or if you modify it.
|
||||||
|
|
||||||
For example, if you distribute copies of the library, whether gratis
|
For example, if you distribute copies of the library, whether gratis
|
||||||
or for a fee, you must give the recipients all the rights that we gave
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
you. You must make sure that they, too, receive or can get the source
|
you. You must make sure that they, too, receive or can get the source
|
||||||
code. If you link other code with the library, you must provide
|
code. If you link a program with the library, you must provide
|
||||||
complete object files to the recipients, so that they can relink them
|
complete object files to the recipients so that they can relink them
|
||||||
with the library after making changes to the library and recompiling
|
with the library, after making changes to the library and recompiling
|
||||||
it. And you must show them these terms so they know their rights.
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
We protect your rights with a two-step method: (1) we copyright the
|
Our method of protecting your rights has two steps: (1) copyright
|
||||||
library, and (2) we offer you this license, which gives you legal
|
the library, and (2) offer you this license which gives you legal
|
||||||
permission to copy, distribute and/or modify the library.
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
To protect each distributor, we want to make it very clear that
|
Also, for each distributor's protection, we want to make certain
|
||||||
there is no warranty for the free library. Also, if the library is
|
that everyone understands that there is no warranty for this free
|
||||||
modified by someone else and passed on, the recipients should know
|
library. If the library is modified by someone else and passed on, we
|
||||||
that what they have is not the original version, so that the original
|
want its recipients to know that what they have is not the original
|
||||||
author's reputation will not be affected by problems that might be
|
version, so that any problems introduced by others will not reflect on
|
||||||
introduced by others.
|
the original authors' reputations.
|
||||||
|
|
||||||
Finally, software patents pose a constant threat to the existence of
|
Finally, any free program is threatened constantly by software
|
||||||
any free program. We wish to make sure that a company cannot
|
patents. We wish to avoid the danger that companies distributing free
|
||||||
effectively restrict the users of a free program by obtaining a
|
software will individually obtain patent licenses, thus in effect
|
||||||
restrictive license from a patent holder. Therefore, we insist that
|
transforming the program into proprietary software. To prevent this,
|
||||||
any patent license obtained for a version of the library must be
|
we have made it clear that any patent must be licensed for everyone's
|
||||||
consistent with the full freedom of use specified in this license.
|
free use or not licensed at all.
|
||||||
|
|
||||||
Most GNU software, including some libraries, is covered by the
|
Most GNU software, including some libraries, is covered by the ordinary
|
||||||
ordinary GNU General Public License. This license, the GNU Lesser
|
GNU General Public License, which was designed for utility programs. This
|
||||||
General Public License, applies to certain designated libraries, and
|
license, the GNU Library General Public License, applies to certain
|
||||||
is quite different from the ordinary General Public License. We use
|
designated libraries. This license is quite different from the ordinary
|
||||||
this license for certain libraries in order to permit linking those
|
one; be sure to read it in full, and don't assume that anything in it is
|
||||||
libraries into non-free programs.
|
the same as in the ordinary license.
|
||||||
|
|
||||||
When a program is linked with a library, whether statically or using
|
The reason we have a separate public license for some libraries is that
|
||||||
a shared library, the combination of the two is legally speaking a
|
they blur the distinction we usually make between modifying or adding to a
|
||||||
combined work, a derivative of the original library. The ordinary
|
program and simply using it. Linking a program with a library, without
|
||||||
General Public License therefore permits such linking only if the
|
changing the library, is in some sense simply using the library, and is
|
||||||
entire combination fits its criteria of freedom. The Lesser General
|
analogous to running a utility program or application program. However, in
|
||||||
Public License permits more lax criteria for linking other code with
|
a textual and legal sense, the linked executable is a combined work, a
|
||||||
the library.
|
derivative of the original library, and the ordinary General Public License
|
||||||
|
treats it as such.
|
||||||
|
|
||||||
We call this license the "Lesser" General Public License because it
|
Because of this blurred distinction, using the ordinary General
|
||||||
does Less to protect the user's freedom than the ordinary General
|
Public License for libraries did not effectively promote software
|
||||||
Public License. It also provides other free software developers Less
|
sharing, because most developers did not use the libraries. We
|
||||||
of an advantage over competing non-free programs. These disadvantages
|
concluded that weaker conditions might promote sharing better.
|
||||||
are the reason we use the ordinary General Public License for many
|
|
||||||
libraries. However, the Lesser license provides advantages in certain
|
|
||||||
special circumstances.
|
|
||||||
|
|
||||||
For example, on rare occasions, there may be a special need to
|
However, unrestricted linking of non-free programs would deprive the
|
||||||
encourage the widest possible use of a certain library, so that it becomes
|
users of those programs of all benefit from the free status of the
|
||||||
a de-facto standard. To achieve this, non-free programs must be
|
libraries themselves. This Library General Public License is intended to
|
||||||
allowed to use the library. A more frequent case is that a free
|
permit developers of non-free programs to use free libraries, while
|
||||||
library does the same job as widely used non-free libraries. In this
|
preserving your freedom as a user of such programs to change the free
|
||||||
case, there is little to gain by limiting the free library to free
|
libraries that are incorporated in them. (We have not seen how to achieve
|
||||||
software only, so we use the Lesser General Public License.
|
this as regards changes in header files, but we have achieved it as regards
|
||||||
|
changes in the actual functions of the Library.) The hope is that this
|
||||||
In other cases, permission to use a particular library in non-free
|
will lead to faster development of free libraries.
|
||||||
programs enables a greater number of people to use a large body of
|
|
||||||
free software. For example, permission to use the GNU C Library in
|
|
||||||
non-free programs enables many more people to use the whole GNU
|
|
||||||
operating system, as well as its variant, the GNU/Linux operating
|
|
||||||
system.
|
|
||||||
|
|
||||||
Although the Lesser General Public License is Less protective of the
|
|
||||||
users' freedom, it does ensure that the user of a program that is
|
|
||||||
linked with the Library has the freedom and the wherewithal to run
|
|
||||||
that program using a modified version of the Library.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow. Pay close attention to the difference between a
|
modification follow. Pay close attention to the difference between a
|
||||||
"work based on the library" and a "work that uses the library". The
|
"work based on the library" and a "work that uses the library". The
|
||||||
former contains code derived from the library, whereas the latter must
|
former contains code derived from the library, while the latter only
|
||||||
be combined with the library in order to run.
|
works together with the library.
|
||||||
|
|
||||||
|
Note that it is possible for a library to be covered by the ordinary
|
||||||
|
General Public License rather than by this special one.
|
||||||
|
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
0. This License Agreement applies to any software library or other
|
0. This License Agreement applies to any software library which
|
||||||
program which contains a notice placed by the copyright holder or
|
contains a notice placed by the copyright holder or other authorized
|
||||||
other authorized party saying it may be distributed under the terms of
|
party saying it may be distributed under the terms of this Library
|
||||||
this Lesser General Public License (also called "this License").
|
General Public License (also called "this License"). Each licensee is
|
||||||
Each licensee is addressed as "you".
|
addressed as "you".
|
||||||
|
|
||||||
A "library" means a collection of software functions and/or data
|
A "library" means a collection of software functions and/or data
|
||||||
prepared so as to be conveniently linked with application programs
|
prepared so as to be conveniently linked with application programs
|
||||||
@ -146,7 +133,7 @@ such a program is covered only if its contents constitute a work based
|
|||||||
on the Library (independent of the use of the Library in a tool for
|
on the Library (independent of the use of the Library in a tool for
|
||||||
writing it). Whether that is true depends on what the Library does
|
writing it). Whether that is true depends on what the Library does
|
||||||
and what the program that uses the Library does.
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Library's
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
complete source code as you receive it, in any medium, provided that
|
complete source code as you receive it, in any medium, provided that
|
||||||
you conspicuously and appropriately publish on each copy an
|
you conspicuously and appropriately publish on each copy an
|
||||||
@ -268,7 +255,7 @@ distribute the object code for the work under the terms of Section 6.
|
|||||||
Any executables containing that work also fall under Section 6,
|
Any executables containing that work also fall under Section 6,
|
||||||
whether or not they are linked directly with the Library itself.
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
6. As an exception to the Sections above, you may also combine or
|
6. As an exception to the Sections above, you may also compile or
|
||||||
link a "work that uses the Library" with the Library to produce a
|
link a "work that uses the Library" with the Library to produce a
|
||||||
work containing portions of the Library, and distribute that work
|
work containing portions of the Library, and distribute that work
|
||||||
under terms of your choice, provided that the terms permit
|
under terms of your choice, provided that the terms permit
|
||||||
@ -295,31 +282,23 @@ of these things:
|
|||||||
Library will not necessarily be able to recompile the application
|
Library will not necessarily be able to recompile the application
|
||||||
to use the modified definitions.)
|
to use the modified definitions.)
|
||||||
|
|
||||||
b) Use a suitable shared library mechanism for linking with the
|
b) Accompany the work with a written offer, valid for at
|
||||||
Library. A suitable mechanism is one that (1) uses at run time a
|
|
||||||
copy of the library already present on the user's computer system,
|
|
||||||
rather than copying library functions into the executable, and (2)
|
|
||||||
will operate properly with a modified version of the library, if
|
|
||||||
the user installs one, as long as the modified version is
|
|
||||||
interface-compatible with the version that the work was made with.
|
|
||||||
|
|
||||||
c) Accompany the work with a written offer, valid for at
|
|
||||||
least three years, to give the same user the materials
|
least three years, to give the same user the materials
|
||||||
specified in Subsection 6a, above, for a charge no more
|
specified in Subsection 6a, above, for a charge no more
|
||||||
than the cost of performing this distribution.
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
d) If distribution of the work is made by offering access to copy
|
c) If distribution of the work is made by offering access to copy
|
||||||
from a designated place, offer equivalent access to copy the above
|
from a designated place, offer equivalent access to copy the above
|
||||||
specified materials from the same place.
|
specified materials from the same place.
|
||||||
|
|
||||||
e) Verify that the user has already received a copy of these
|
d) Verify that the user has already received a copy of these
|
||||||
materials or that you have already sent this user a copy.
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
For an executable, the required form of the "work that uses the
|
For an executable, the required form of the "work that uses the
|
||||||
Library" must include any data and utility programs needed for
|
Library" must include any data and utility programs needed for
|
||||||
reproducing the executable from it. However, as a special exception,
|
reproducing the executable from it. However, as a special exception,
|
||||||
the materials to be distributed need not include anything that is
|
the source code distributed need not include anything that is normally
|
||||||
normally distributed (in either source or binary form) with the major
|
distributed (in either source or binary form) with the major
|
||||||
components (compiler, kernel, and so on) of the operating system on
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
which the executable runs, unless that component itself accompanies
|
which the executable runs, unless that component itself accompanies
|
||||||
the executable.
|
the executable.
|
||||||
@ -368,7 +347,7 @@ Library), the recipient automatically receives a license from the
|
|||||||
original licensor to copy, distribute, link with or modify the Library
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
subject to these terms and conditions. You may not impose any further
|
subject to these terms and conditions. You may not impose any further
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
You are not responsible for enforcing compliance by third parties with
|
You are not responsible for enforcing compliance by third parties to
|
||||||
this License.
|
this License.
|
||||||
|
|
||||||
11. If, as a consequence of a court judgment or allegation of patent
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
@ -411,7 +390,7 @@ excluded. In such case, this License incorporates the limitation as if
|
|||||||
written in the body of this License.
|
written in the body of this License.
|
||||||
|
|
||||||
13. The Free Software Foundation may publish revised and/or new
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
versions of the Lesser General Public License from time to time.
|
versions of the Library General Public License from time to time.
|
||||||
Such new versions will be similar in spirit to the present version,
|
Such new versions will be similar in spirit to the present version,
|
||||||
but may differ in detail to address new problems or concerns.
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
@ -474,16 +453,16 @@ convey the exclusion of warranty; and each file should have at least the
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
This library is free software; you can redistribute it and/or
|
||||||
modify it under the terms of the GNU Lesser General Public
|
modify it under the terms of the GNU Library General Public
|
||||||
License as published by the Free Software Foundation; either
|
License as published by the Free Software Foundation; either
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
This library is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
Lesser General Public License for more details.
|
Library General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
You should have received a copy of the GNU Library General Public
|
||||||
License along with this library; if not, write to the Free Software
|
License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
64
README.rst
64
README.rst
@ -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
|
As far as I know, sshuttle is the only program that solves the following
|
||||||
common case:
|
common case:
|
||||||
|
|
||||||
- Your client machine (or router) is Linux, FreeBSD, MacOS or Windows.
|
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
|
||||||
|
|
||||||
- You have access to a remote network via ssh.
|
- You have access to a remote network via ssh.
|
||||||
|
|
||||||
@ -24,15 +24,69 @@ common case:
|
|||||||
- You can't use openssh's PermitTunnel feature because
|
- You can't use openssh's PermitTunnel feature because
|
||||||
it's disabled by default on openssh servers; plus it does
|
it's disabled by default on openssh servers; plus it does
|
||||||
TCP-over-TCP, which has `terrible performance`_.
|
TCP-over-TCP, which has `terrible performance`_.
|
||||||
|
|
||||||
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
|
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
|
||||||
|
|
||||||
Obtaining sshuttle
|
Obtaining sshuttle
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Please see the documentation_.
|
- Debian stretch or later::
|
||||||
|
|
||||||
|
apt-get install sshuttle
|
||||||
|
|
||||||
|
- Arch Linux::
|
||||||
|
|
||||||
|
pacman -S sshuttle
|
||||||
|
|
||||||
|
- Fedora::
|
||||||
|
|
||||||
|
dnf install sshuttle
|
||||||
|
|
||||||
|
- NixOS::
|
||||||
|
|
||||||
|
nix-env -iA nixos.sshuttle
|
||||||
|
|
||||||
|
- From PyPI::
|
||||||
|
|
||||||
|
sudo pip install sshuttle
|
||||||
|
|
||||||
|
- Clone::
|
||||||
|
|
||||||
|
git clone https://github.com/sshuttle/sshuttle.git
|
||||||
|
cd sshuttle
|
||||||
|
sudo ./setup.py install
|
||||||
|
|
||||||
|
- FreeBSD::
|
||||||
|
|
||||||
|
# ports
|
||||||
|
cd /usr/ports/net/py-sshuttle && make install clean
|
||||||
|
# pkg
|
||||||
|
pkg install py36-sshuttle
|
||||||
|
|
||||||
|
It is also possible to install into a virtualenv as a non-root user.
|
||||||
|
|
||||||
|
- From PyPI::
|
||||||
|
|
||||||
|
virtualenv -p python3 /tmp/sshuttle
|
||||||
|
. /tmp/sshuttle/bin/activate
|
||||||
|
pip install sshuttle
|
||||||
|
|
||||||
|
- Clone::
|
||||||
|
|
||||||
|
virtualenv -p python3 /tmp/sshuttle
|
||||||
|
. /tmp/sshuttle/bin/activate
|
||||||
|
git clone https://github.com/sshuttle/sshuttle.git
|
||||||
|
cd sshuttle
|
||||||
|
./setup.py install
|
||||||
|
|
||||||
|
- Homebrew::
|
||||||
|
|
||||||
|
brew install sshuttle
|
||||||
|
|
||||||
|
- Nix::
|
||||||
|
|
||||||
|
nix-env -iA nixpkgs.sshuttle
|
||||||
|
|
||||||
.. _Documentation: https://sshuttle.readthedocs.io/en/stable/installation.html
|
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
@ -45,5 +99,5 @@ https://sshuttle.readthedocs.org/en/latest/
|
|||||||
|
|
||||||
Running as a service
|
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
|
https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe
|
||||||
|
76
bin/sudoers-add
Executable file
76
bin/sudoers-add
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# William Mantly <wmantly@gmail.com>
|
||||||
|
# MIT License
|
||||||
|
# https://github.com/wmantly/sudoers-add
|
||||||
|
|
||||||
|
NEWLINE=$'\n'
|
||||||
|
CONTENT=""
|
||||||
|
ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")"
|
||||||
|
|
||||||
|
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
|
||||||
|
echo "Usage: $ME [file_path] [sudoers-file-name]"
|
||||||
|
echo "Usage: [content] | $ME sudoers-file-name"
|
||||||
|
echo "This will take a sudoers config validate it and add it to /etc/sudoers.d/{sudoers-file-name}"
|
||||||
|
echo "The config can come from a file, first usage example or piped in second example."
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" == "" ]; then
|
||||||
|
(>&2 echo "This command take at lest one argument. See $ME --help")
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$2" == "" ]; then
|
||||||
|
FILE_NAME=$1
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
FILE_NAME=$2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "This script must be run as root"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while read -r line
|
||||||
|
do
|
||||||
|
CONTENT+="${line}${NEWLINE}"
|
||||||
|
done < "${1:-/dev/stdin}"
|
||||||
|
|
||||||
|
if [ "$CONTENT" == "" ]; then
|
||||||
|
(>&2 echo "No config content specified. See $ME --help")
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FILE_NAME" == "" ]; then
|
||||||
|
(>&2 echo "No sudoers file name specified. See $ME --help")
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make a temp file to hold the sudoers config
|
||||||
|
umask 077
|
||||||
|
TEMP_FILE=$(mktemp)
|
||||||
|
echo "$CONTENT" > "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Make sure the content is valid
|
||||||
|
visudo_STDOUT=$(visudo -c -f "$TEMP_FILE" 2>&1)
|
||||||
|
visudo_code=$?
|
||||||
|
# The temp file is no longer needed
|
||||||
|
rm "$TEMP_FILE"
|
||||||
|
|
||||||
|
if [ $visudo_code -eq 0 ]; then
|
||||||
|
echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME"
|
||||||
|
chmod 0440 "/etc/sudoers.d/$FILE_NAME"
|
||||||
|
echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Invalid sudoers config!"
|
||||||
|
echo "$visudo_STDOUT"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -9,3 +9,4 @@ stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and
|
|||||||
it just works, as do xterms and ssvncviewer etc.
|
it just works, as do xterms and ssvncviewer etc.
|
||||||
|
|
||||||
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide
|
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
import sshuttle # NOQA
|
import sshuttle.version # NOQA
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
# 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
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
@ -56,7 +56,7 @@ copyright = '2016, Brian May'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = sshuttle.__version__
|
release = sshuttle.version.version
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '.'.join(release.split('.')[:2])
|
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
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = 'furo'
|
html_theme = 'default'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# 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
|
# further. For a list of options available for each theme, see the
|
||||||
|
@ -34,3 +34,4 @@ sshuttle assembles the TCP stream locally, multiplexes it statefully over
|
|||||||
an ssh session, and disassembles it back into packets at the other end. So
|
an ssh session, and disassembles it back into packets at the other end. So
|
||||||
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
||||||
safe.
|
safe.
|
||||||
|
|
||||||
|
@ -26,3 +26,4 @@ Indices and tables
|
|||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
|
@ -1,84 +1,22 @@
|
|||||||
Installation
|
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::
|
- From PyPI::
|
||||||
|
|
||||||
sudo pip install sshuttle
|
pip install sshuttle
|
||||||
|
|
||||||
|
- Debain package manager::
|
||||||
|
sudo apt install sshuttle
|
||||||
|
|
||||||
- Clone::
|
- Clone::
|
||||||
|
|
||||||
git clone https://github.com/sshuttle/sshuttle.git
|
git clone https://github.com/sshuttle/sshuttle.git
|
||||||
cd sshuttle
|
cd sshuttle
|
||||||
sudo ./setup.py install
|
./setup.py install
|
||||||
|
|
||||||
- FreeBSD::
|
|
||||||
|
|
||||||
# ports
|
Optionally after installation
|
||||||
cd /usr/ports/net/py-sshuttle && make install clean
|
-----------------------------
|
||||||
# pkg
|
|
||||||
pkg install py39-sshuttle
|
|
||||||
|
|
||||||
- OpenBSD::
|
- Add to sudoers file
|
||||||
|
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
|
|
||||||
|
257
docs/manpage.rst
257
docs/manpage.rst
@ -4,14 +4,14 @@ sshuttle
|
|||||||
|
|
||||||
Synopsis
|
Synopsis
|
||||||
--------
|
--------
|
||||||
**sshuttle** [*options*] **-r** *[username@]sshserver[:port]* \<*subnets* ...\>
|
**sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\>
|
||||||
|
|
||||||
|
|
||||||
Description
|
Description
|
||||||
-----------
|
-----------
|
||||||
:program:`sshuttle` allows you to create a VPN connection from your
|
:program:`sshuttle` allows you to create a VPN connection from your
|
||||||
machine to any remote server that you can connect to via ssh, as long
|
machine to any remote server that you can connect to via
|
||||||
as that server has a sufficiently new Python installation.
|
ssh, as long as that server has python 2.3 or higher.
|
||||||
|
|
||||||
To work, you must have root access on the local machine,
|
To work, you must have root access on the local machine,
|
||||||
but you can have a normal account on the server.
|
but you can have a normal account on the server.
|
||||||
@ -31,25 +31,20 @@ Options
|
|||||||
.. option:: <subnets>
|
.. option:: <subnets>
|
||||||
|
|
||||||
A list of subnets to route over the VPN, in the form
|
A list of subnets to route over the VPN, in the form
|
||||||
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
|
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
|
||||||
single IP address) and 1.2.3.4/32 (equivalent to 1.2.3.4),
|
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).
|
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
|
||||||
Specify subnets 0/0 to match all IPv4 addresses and ::/0 to match
|
netmask), and 0/0 ('just route everything through the
|
||||||
all IPv6 addresses. Any of the previous examples are also valid if
|
VPN'). Any of the previous examples are also valid if you append
|
||||||
you append a port or a port range, so 1.2.3.4:8000 will only
|
a port or a port range, so 1.2.3.4:8000 will only tunnel traffic
|
||||||
tunnel traffic that has as the destination port 8000 of 1.2.3.4
|
that has as the destination port 8000 of 1.2.3.4 and
|
||||||
and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port
|
1.2.3.0/24:8000-9000 will tunnel traffic going to any port between
|
||||||
between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24
|
8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet.
|
||||||
subnet. A hostname can be provided instead of an IP address. If
|
It is also possible to use a name in which case the first IP it resolves
|
||||||
the hostname resolves to multiple IPs, all of the IPs are
|
to during startup will be routed over the VPN. Valid examples are
|
||||||
included. If a width is provided with a hostname, the width is
|
example.com, example.com:8000 and example.com:8000-9000.
|
||||||
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>
|
.. option:: --method <auto|nat|nft|tproxy|pf>
|
||||||
|
|
||||||
Which firewall method should sshuttle use? For auto, sshuttle attempts to
|
Which firewall method should sshuttle use? For auto, sshuttle attempts to
|
||||||
guess the appropriate method depending on what it can find in PATH. The
|
guess the appropriate method depending on what it can find in PATH. The
|
||||||
@ -69,9 +64,8 @@ Options
|
|||||||
You can use any name resolving to an IP address of the machine running
|
You can use any name resolving to an IP address of the machine running
|
||||||
:program:`sshuttle`, e.g. ``--listen localhost``.
|
:program:`sshuttle`, e.g. ``--listen localhost``.
|
||||||
|
|
||||||
For the nft, tproxy and pf methods this can be an IPv6 address. Use
|
For the tproxy and pf methods this can be an IPv6 address. Use this option
|
||||||
this option with comma separated values if required, to provide both
|
twice if required, to provide both IPv4 and IPv6 addresses.
|
||||||
IPv4 and IPv6 addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``.
|
|
||||||
|
|
||||||
.. option:: -H, --auto-hosts
|
.. option:: -H, --auto-hosts
|
||||||
|
|
||||||
@ -89,13 +83,6 @@ Options
|
|||||||
few subnets over the VPN, you probably would prefer to
|
few subnets over the VPN, you probably would prefer to
|
||||||
keep using your local DNS server for everything else.
|
keep using your local DNS server for everything else.
|
||||||
|
|
||||||
:program:`sshuttle` tries to store a cache of the hostnames in
|
|
||||||
~/.sshuttle.hosts on the remote host. Similarly, it tries to read
|
|
||||||
the file when you later reconnect to the host with --auto-hosts
|
|
||||||
enabled to quickly populate the host list. When troubleshooting
|
|
||||||
this feature, try removing this file on the remote host when
|
|
||||||
sshuttle is not running.
|
|
||||||
|
|
||||||
.. option:: -N, --auto-nets
|
.. option:: -N, --auto-nets
|
||||||
|
|
||||||
In addition to the subnets provided on the command
|
In addition to the subnets provided on the command
|
||||||
@ -104,20 +91,14 @@ Options
|
|||||||
are taken automatically from the server's routing
|
are taken automatically from the server's routing
|
||||||
table.
|
table.
|
||||||
|
|
||||||
This feature does not detect IPv6 routes. Specify IPv6 subnets
|
|
||||||
manually. For example, specify the ``::/0`` subnet on the command
|
|
||||||
line to route all IPv6 traffic.
|
|
||||||
|
|
||||||
.. option:: --dns
|
.. option:: --dns
|
||||||
|
|
||||||
Capture local DNS requests and forward to the remote DNS
|
Capture local DNS requests and forward to the remote DNS
|
||||||
server. All queries to any of the local system's DNS
|
server. All queries to any of the local system's DNS
|
||||||
servers (/etc/resolv.conf and, if it exists,
|
servers (/etc/resolv.conf) will be intercepted and
|
||||||
/run/systemd/resolve/resolv.conf) will be intercepted and
|
|
||||||
resolved on the remote side of the tunnel instead, there
|
resolved on the remote side of the tunnel instead, there
|
||||||
using the DNS specified via the :option:`--to-ns` option,
|
using the DNS specified via the :option:`--to-ns` option,
|
||||||
if specified. Only plain DNS traffic sent to these servers
|
if specified.
|
||||||
on port 53 are captured.
|
|
||||||
|
|
||||||
.. option:: --ns-hosts=<server1[,server2[,server3[...]]]>
|
.. option:: --ns-hosts=<server1[,server2[,server3[...]]]>
|
||||||
|
|
||||||
@ -140,19 +121,16 @@ Options
|
|||||||
|
|
||||||
.. option:: --python
|
.. option:: --python
|
||||||
|
|
||||||
Specify the name/path of the remote python interpreter. The
|
Specify the name/path of the remote python interpreter.
|
||||||
default is to use ``python3`` (or ``python``, if ``python3``
|
The default is just ``python``, which means to use the
|
||||||
fails) in the remote system's PATH.
|
default python interpreter on the remote system's PATH.
|
||||||
|
|
||||||
.. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
|
.. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
|
||||||
|
|
||||||
The remote hostname and optional username and ssh
|
The remote hostname and optional username and ssh
|
||||||
port number to use for connecting to the remote server.
|
port number to use for connecting to the remote server.
|
||||||
For example, example.com, testuser@example.com,
|
For example, example.com, testuser@example.com,
|
||||||
testuser@example.com:2222, or example.com:2244. This
|
testuser@example.com:2222, or example.com:2244.
|
||||||
hostname is passed to ssh, so it will recognize any
|
|
||||||
aliases and settings you may have configured in
|
|
||||||
~/.ssh/config.
|
|
||||||
|
|
||||||
.. option:: -x <subnet>, --exclude=<subnet>
|
.. option:: -x <subnet>, --exclude=<subnet>
|
||||||
|
|
||||||
@ -181,23 +159,11 @@ Options
|
|||||||
in a non-standard location or you want to provide extra
|
in a non-standard location or you want to provide extra
|
||||||
options to the ssh command, for example, ``-e 'ssh -v'``.
|
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
|
.. option:: --seed-hosts
|
||||||
|
|
||||||
A comma-separated list of hostnames to use to
|
A comma-separated list of hostnames to use to
|
||||||
initialize the :option:`--auto-hosts` scan algorithm.
|
initialize the :option:`--auto-hosts` scan algorithm.
|
||||||
:option:`--auto-hosts` does things like poll netstat output
|
:option:`--auto-hosts` does things like poll local SMB servers
|
||||||
for lists of local hostnames, but can speed things up
|
for lists of local hostnames, but can speed things up
|
||||||
if you use this option to give it a few names to start
|
if you use this option to give it a few names to start
|
||||||
from.
|
from.
|
||||||
@ -235,11 +201,6 @@ Options
|
|||||||
Automatically fork into the background after connecting
|
Automatically fork into the background after connecting
|
||||||
to the remote server. Implies :option:`--syslog`.
|
to the remote server. Implies :option:`--syslog`.
|
||||||
|
|
||||||
.. option:: -s <file>, --subnets=<file>
|
|
||||||
|
|
||||||
Include the subnets specified in a file instead of on the
|
|
||||||
command line. One subnet per line.
|
|
||||||
|
|
||||||
.. option:: --syslog
|
.. option:: --syslog
|
||||||
|
|
||||||
after connecting, send all log messages to the
|
after connecting, send all log messages to the
|
||||||
@ -254,8 +215,7 @@ Options
|
|||||||
|
|
||||||
.. option:: --disable-ipv6
|
.. option:: --disable-ipv6
|
||||||
|
|
||||||
Disable IPv6 support for methods that support it (nat, nft,
|
If using tproxy or pf methods, this will disable IPv6 support.
|
||||||
tproxy, and pf).
|
|
||||||
|
|
||||||
.. option:: --firewall
|
.. option:: --firewall
|
||||||
|
|
||||||
@ -274,28 +234,28 @@ Options
|
|||||||
makes it a lot easier to debug and test the :option:`--auto-hosts`
|
makes it a lot easier to debug and test the :option:`--auto-hosts`
|
||||||
feature.
|
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
|
.. option:: --sudoers-no-modify
|
||||||
|
|
||||||
sshuttle prints a configuration to stdout which allows a user to
|
sshuttle will auto generate the proper sudoers.d config and print it to
|
||||||
run sshuttle without a password. This option is INSECURE because,
|
stdout. The option will not modify the system at all.
|
||||||
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
|
.. option:: --sudoers-user
|
||||||
|
|
||||||
Set the user name or group with %group_name for passwordless
|
Set the user name or group with %group_name for passwordless operation.
|
||||||
operation. Default is the current user. Set to ALL for all users
|
Default is the current user.set ALL for all users. Only works with
|
||||||
(NOT RECOMMENDED: See note about security in --sudoers-no-modify
|
--sudoers or --sudoers-no-modify option.
|
||||||
documentation above). Only works with the --sudoers-no-modify
|
|
||||||
option.
|
|
||||||
|
|
||||||
.. option:: -t <mark>, --tmark=<mark>
|
.. option:: --sudoers-filename
|
||||||
|
|
||||||
An option used by the tproxy method: Use the specified traffic
|
Set the file name for the sudoers.d file to be added. Default is
|
||||||
mark. The mark must be a hexadecimal value. Defaults to 0x01.
|
"sshuttle_auto". Only works with --sudoers.
|
||||||
|
|
||||||
.. option:: --version
|
.. option:: --version
|
||||||
|
|
||||||
@ -324,119 +284,54 @@ Arguments read from a file must be one per line, as shown below::
|
|||||||
--option2
|
--option2
|
||||||
value2
|
value2
|
||||||
|
|
||||||
The configuration file supports comments for human-readable
|
|
||||||
annotations. For example::
|
|
||||||
|
|
||||||
# company-internal API
|
|
||||||
8.8.8.8/32
|
|
||||||
# home IoT
|
|
||||||
192.168.63.0/24
|
|
||||||
|
|
||||||
|
|
||||||
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
|
Examples
|
||||||
--------
|
--------
|
||||||
|
Test locally by proxying all local connections, without using ssh::
|
||||||
|
|
||||||
Use the following command to route all IPv4 TCP traffic through remote
|
$ sshuttle -v 0/0
|
||||||
(-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::
|
|
||||||
|
|
||||||
$ sshuttle -r example.com -x example.com:22 0/0
|
Starting sshuttle proxy.
|
||||||
|
Listening on ('0.0.0.0', 12300).
|
||||||
Starting sshuttle proxy (version ...).
|
|
||||||
[local sudo] Password:
|
[local sudo] Password:
|
||||||
fw: Starting firewall with Python version 3.9.5
|
firewall manager ready.
|
||||||
fw: ready method name nat.
|
c : connecting to server...
|
||||||
c : IPv6 disabled since it isn't supported by method nat.
|
s: available routes:
|
||||||
c : Method: nat
|
s: 192.168.42.0/24
|
||||||
c : IPv4: on
|
c : connected.
|
||||||
c : IPv6: off (not available with nat method)
|
firewall manager: starting transproxy.
|
||||||
c : UDP : off (not available with nat method)
|
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
|
||||||
c : DNS : off (available)
|
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
|
||||||
c : User: off (available)
|
...etc...
|
||||||
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
|
|
||||||
c : (<AddressFamily.AF_INET: 2>, '0.0.0.0', 0, 0, 0)
|
|
||||||
c : Subnets to exclude from forwarding:
|
|
||||||
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
|
|
||||||
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
|
|
||||||
c : TCP redirector listening on ('127.0.0.1', 12299).
|
|
||||||
c : Starting client with Python version 3.9.5
|
|
||||||
c : Connecting to server...
|
|
||||||
user@example.com's password:
|
|
||||||
s: Starting server with Python version 3.6.8
|
|
||||||
s: latency control setting = True
|
|
||||||
s: auto-nets:False
|
|
||||||
c : Connected to server.
|
|
||||||
fw: setting up.
|
|
||||||
fw: iptables -w -t nat -N sshuttle-12299
|
|
||||||
fw: iptables -w -t nat -F sshuttle-12299
|
|
||||||
...
|
|
||||||
Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
|
||||||
^C
|
^C
|
||||||
|
firewall manager: undoing changes.
|
||||||
|
KeyboardInterrupt
|
||||||
c : Keyboard interrupt: exiting.
|
c : Keyboard interrupt: exiting.
|
||||||
c : SW'unknown':Mux#1: deleting (1 remain)
|
c : SW#8:192.168.42.121:47523: deleting
|
||||||
c : SW#7:192.168.42.121:60554: deleting (0 remain)
|
c : SW#6:192.168.42.106:50035: deleting
|
||||||
|
|
||||||
|
Test connection to a remote server, with automatic hostname
|
||||||
Connect to a remote server, with automatic hostname
|
|
||||||
and subnet guessing::
|
and subnet guessing::
|
||||||
|
|
||||||
$ sshuttle -vNHr example.com -x example.com:22
|
$ sshuttle -vNHr example.org
|
||||||
Starting sshuttle proxy (version ...).
|
|
||||||
[local sudo] Password:
|
Starting sshuttle proxy.
|
||||||
fw: Starting firewall with Python version 3.9.5
|
Listening on ('0.0.0.0', 12300).
|
||||||
fw: ready method name nat.
|
firewall manager ready.
|
||||||
c : IPv6 disabled since it isn't supported by method nat.
|
c : connecting to server...
|
||||||
c : Method: nat
|
|
||||||
c : IPv4: on
|
|
||||||
c : IPv6: off (not available with nat method)
|
|
||||||
c : UDP : off (not available with nat method)
|
|
||||||
c : DNS : off (available)
|
|
||||||
c : User: off (available)
|
|
||||||
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
|
|
||||||
c : NOTE: Additional subnets to forward may be added below by --auto-nets.
|
|
||||||
c : Subnets to exclude from forwarding:
|
|
||||||
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
|
|
||||||
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
|
|
||||||
c : TCP redirector listening on ('127.0.0.1', 12300).
|
|
||||||
c : Starting client with Python version 3.9.5
|
|
||||||
c : Connecting to server...
|
|
||||||
user@example.com's password:
|
|
||||||
s: Starting server with Python version 3.6.8
|
|
||||||
s: latency control setting = True
|
|
||||||
s: auto-nets:True
|
|
||||||
c : Connected to server.
|
|
||||||
c : seed_hosts: []
|
|
||||||
s: available routes:
|
s: available routes:
|
||||||
s: 77.141.99.0/24
|
s: 77.141.99.0/24
|
||||||
fw: setting up.
|
c : connected.
|
||||||
fw: iptables -w -t nat -N sshuttle-12300
|
c : seed_hosts: []
|
||||||
fw: iptables -w -t nat -F sshuttle-12300
|
firewall manager: starting transproxy.
|
||||||
...
|
hostwatch: Found: testbox1: 1.2.3.4
|
||||||
|
hostwatch: Found: mytest2: 5.6.7.8
|
||||||
|
hostwatch: Found: domaincontroller: 99.1.2.3
|
||||||
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
||||||
^C
|
^C
|
||||||
|
firewall manager: undoing changes.
|
||||||
c : Keyboard interrupt: exiting.
|
c : Keyboard interrupt: exiting.
|
||||||
c : SW'unknown':Mux#1: deleting (1 remain)
|
c : SW#6:192.168.42.121:60554: deleting
|
||||||
c : SW#7:192.168.42.121:60554: deleting (0 remain)
|
|
||||||
|
|
||||||
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
|
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
|
||||||
|
|
||||||
@ -460,7 +355,9 @@ Example configuration file::
|
|||||||
Discussion
|
Discussion
|
||||||
----------
|
----------
|
||||||
When it starts, :program:`sshuttle` creates an ssh session to the
|
When it starts, :program:`sshuttle` creates an ssh session to the
|
||||||
server specified by the ``-r`` option.
|
server specified by the ``-r`` option. If ``-r`` is omitted,
|
||||||
|
it will start both its client and server locally, which is
|
||||||
|
sometimes useful for testing.
|
||||||
|
|
||||||
After connecting to the remote server, :program:`sshuttle` uploads its
|
After connecting to the remote server, :program:`sshuttle` uploads its
|
||||||
(python) source code to the remote end and executes it
|
(python) source code to the remote end and executes it
|
||||||
@ -479,7 +376,7 @@ Packet-level forwarding (eg. using the tun/tap devices on
|
|||||||
Linux) seems elegant at first, but it results in
|
Linux) seems elegant at first, but it results in
|
||||||
several problems, notably the 'tcp over tcp' problem. The
|
several problems, notably the 'tcp over tcp' problem. The
|
||||||
tcp protocol depends fundamentally on packets being dropped
|
tcp protocol depends fundamentally on packets being dropped
|
||||||
in order to implement its congestion control algorithm; if
|
in order to implement its congestion control agorithm; if
|
||||||
you pass tcp packets through a tcp-based tunnel (such as
|
you pass tcp packets through a tcp-based tunnel (such as
|
||||||
ssh), the inner tcp packets will never be dropped, and so
|
ssh), the inner tcp packets will never be dropped, and so
|
||||||
the inner tcp stream's congestion control will be
|
the inner tcp stream's congestion control will be
|
||||||
|
@ -3,6 +3,6 @@ OpenWRT
|
|||||||
|
|
||||||
Run::
|
Run::
|
||||||
|
|
||||||
opkg install python3 python3-pip iptables-mod-extra iptables-mod-nat-extra iptables-mod-ipopt
|
opkg install python3 python3-pip iptables-mod-nat-extra iptables-mod-ipopt
|
||||||
python3 /usr/bin/pip3 install sshuttle
|
python3 /usr/bin/pip3 install sshuttle
|
||||||
sshuttle -l 0.0.0.0 -r <IP> -x 192.168.1.1 0/0
|
sshuttle -l 0.0.0.0 -r <IP> -x 192.168.1.1 0/0
|
||||||
|
@ -6,7 +6,7 @@ Client side Requirements
|
|||||||
|
|
||||||
- sudo, or root access on your client machine.
|
- sudo, or root access on your client machine.
|
||||||
(The server doesn't need admin access.)
|
(The server doesn't need admin access.)
|
||||||
- Python 3.9 or greater.
|
- Python 3.5 or greater.
|
||||||
|
|
||||||
|
|
||||||
Linux with NAT method
|
Linux with NAT method
|
||||||
@ -15,36 +15,22 @@ Supports:
|
|||||||
|
|
||||||
* IPv4 TCP
|
* IPv4 TCP
|
||||||
* IPv4 DNS
|
* IPv4 DNS
|
||||||
* IPv6 TCP
|
|
||||||
* IPv6 DNS
|
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* iptables DNAT and REDIRECT modules. ip6tables for IPv6.
|
* iptables DNAT, REDIRECT, and ttl modules.
|
||||||
|
|
||||||
Linux with nft method
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
Supports
|
|
||||||
|
|
||||||
* IPv4 TCP
|
|
||||||
* IPv4 DNS
|
|
||||||
* IPv6 TCP
|
|
||||||
* IPv6 DNS
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* nftables
|
|
||||||
|
|
||||||
Linux with TPROXY method
|
Linux with TPROXY method
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Supports:
|
Supports:
|
||||||
|
|
||||||
* IPv4 TCP
|
* IPv4 TCP
|
||||||
* IPv4 UDP
|
* IPv4 UDP (requires ``recvmsg`` - see below)
|
||||||
* IPv4 DNS
|
* IPv6 DNS (requires ``recvmsg`` - see below)
|
||||||
* IPv6 TCP
|
* IPv6 TCP
|
||||||
* IPv6 UDP
|
* IPv6 UDP (requires ``recvmsg`` - see below)
|
||||||
* IPv6 DNS
|
* IPv6 DNS (requires ``recvmsg`` - see below)
|
||||||
|
|
||||||
|
|
||||||
MacOS / FreeBSD / OpenBSD / pfSense
|
MacOS / FreeBSD / OpenBSD / pfSense
|
||||||
@ -65,13 +51,15 @@ Requires:
|
|||||||
Windows
|
Windows
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
Experimental built-in support available. See :doc:`windows` for more information.
|
Not officially supported, however can be made to work with Vagrant. Requires
|
||||||
|
cmd.exe with Administrator access. See :doc:`windows` for more information.
|
||||||
|
|
||||||
|
|
||||||
Server side Requirements
|
Server side Requirements
|
||||||
------------------------
|
------------------------
|
||||||
|
The server can run in any version of Python between 2.4 and 3.6.
|
||||||
- Python 3.9 or greater.
|
However it is recommended that you use Python 2.7, Python 3.5 or later whenever
|
||||||
|
possible as support for older versions might be dropped in the future.
|
||||||
|
|
||||||
|
|
||||||
Additional Suggested Software
|
Additional Suggested Software
|
||||||
@ -80,7 +68,7 @@ Additional Suggested Software
|
|||||||
- If you are using systemd, sshuttle can notify it when the connection to
|
- 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
|
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
|
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
|
.. code-block:: ini
|
||||||
:emphasize-lines: 6
|
:emphasize-lines: 6
|
||||||
@ -88,10 +76,10 @@ Additional Suggested Software
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=sshuttle
|
Description=sshuttle
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=notify
|
||||||
ExecStart=/usr/bin/sshuttle --dns --remote <user>@<server> <subnets...>
|
ExecStart=/usr/bin/sshuttle --dns --remote <user>@<server> <subnets...>
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
TPROXY
|
TPROXY
|
||||||
======
|
======
|
||||||
TPROXY is the only method that supports UDP.
|
TPROXY is the only method that has full support of IPv6 and UDP.
|
||||||
|
|
||||||
There are some things you need to consider for TPROXY to work:
|
There are some things you need to consider for TPROXY to work:
|
||||||
|
|
||||||
@ -8,24 +8,26 @@ There are some things you need to consider for TPROXY to work:
|
|||||||
done once after booting up::
|
done once after booting up::
|
||||||
|
|
||||||
ip route add local default dev lo table 100
|
ip route add local default dev lo table 100
|
||||||
ip rule add fwmark {TMARK} lookup 100
|
ip rule add fwmark 1 lookup 100
|
||||||
ip -6 route add local default dev lo table 100
|
ip -6 route add local default dev lo table 100
|
||||||
ip -6 rule add fwmark {TMARK} lookup 100
|
ip -6 rule add fwmark 1 lookup 100
|
||||||
|
|
||||||
where {TMARK} is the identifier mark passed with -t or --tmark flag
|
|
||||||
as a hexadecimal string (default value is '0x01').
|
|
||||||
|
|
||||||
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
|
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
|
||||||
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
|
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
|
||||||
|
|
||||||
- The client needs to be run as root. e.g.::
|
- The client needs to be run as root. e.g.::
|
||||||
|
|
||||||
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
||||||
|
|
||||||
- You may need to exclude the IP address of the server you are connecting to.
|
- You may need to exclude the IP address of the server you are connecting to.
|
||||||
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
|
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
|
||||||
work. Use the ``--exclude`` parameter for this.
|
work. Use the ``--exclude`` parameter for this.
|
||||||
|
|
||||||
|
- Similarly, UDP return packets (including DNS) could get intercepted and
|
||||||
|
bounced back. This is the case if you have a broad subnet such as
|
||||||
|
``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the
|
||||||
|
``--exclude`` parameter for this.
|
||||||
|
|
||||||
- You need the ``--method=tproxy`` parameter, as above.
|
- You need the ``--method=tproxy`` parameter, as above.
|
||||||
|
|
||||||
- The routes for the outgoing packets must already exist. For example, if your
|
- The routes for the outgoing packets must already exist. For example, if your
|
||||||
|
@ -33,3 +33,4 @@ That project I did for Slipstream was what first gave me the idea to merge
|
|||||||
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
||||||
program that was the best of all worlds. And here we are, at last.
|
program that was the best of all worlds. And here we are, at last.
|
||||||
You're welcome.
|
You're welcome.
|
||||||
|
|
||||||
|
@ -11,10 +11,6 @@ Forward all traffic::
|
|||||||
sshuttle -r username@sshserver 0.0.0.0/0
|
sshuttle -r username@sshserver 0.0.0.0/0
|
||||||
|
|
||||||
- Use the :option:`sshuttle -r` parameter to specify a remote server.
|
- Use the :option:`sshuttle -r` parameter to specify a remote server.
|
||||||
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
|
- By default sshuttle will automatically choose a method to use. Override with
|
||||||
the :option:`sshuttle --method` parameter.
|
the :option:`sshuttle --method` parameter.
|
||||||
@ -25,7 +21,7 @@ Forward all traffic::
|
|||||||
sshuttle -r username@sshserver 0/0
|
sshuttle -r username@sshserver 0/0
|
||||||
|
|
||||||
|
|
||||||
- For 'My VPN broke and need a temporary solution FAST to access local IPv4 addresses'::
|
- For 'My VPN broke and need a temporary solution FAST to access local IPv4 addresses':
|
||||||
|
|
||||||
sshuttle --dns -NHr username@sshserver 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
sshuttle --dns -NHr username@sshserver 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||||
|
|
||||||
@ -51,7 +47,7 @@ were right there. And if your "client" machine is a router, everyone on
|
|||||||
your local network can make connections to your remote network.
|
your local network can make connections to your remote network.
|
||||||
|
|
||||||
You don't need to install sshuttle on the remote server;
|
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
|
sshuttle will automatically upload and run its source code
|
||||||
to the remote python interpreter.
|
to the remote python interpreter.
|
||||||
|
|
||||||
@ -71,23 +67,44 @@ admin access on the server.
|
|||||||
|
|
||||||
Sudoers File
|
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 can generate a sudoers.d file for Linux and MacOS. This
|
sshuttle --sudoers
|
||||||
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.
|
|
||||||
|
|
||||||
To print a sudo configuration file and see a suggested way to install it, run::
|
DO NOT run this command with sudo, it will ask for your sudo password when
|
||||||
|
it is needed.
|
||||||
|
|
||||||
|
A costume user or group can be set with the :
|
||||||
|
option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid
|
||||||
|
values for this vary based on how your system is configured. Values such as
|
||||||
|
usernames, groups pre-pended with `%` and sudoers user aliases will work. See
|
||||||
|
the sudoers manual for more information on valid user specif actions.
|
||||||
|
The options must be used with `--sudoers`
|
||||||
|
|
||||||
|
sshuttle --sudoers --sudoers-user mike
|
||||||
|
sshuttle --sudoers --sudoers-user %sudo
|
||||||
|
|
||||||
|
The name of the file to be added to sudoers.d can be configured as well. This
|
||||||
|
is mostly not necessary but can be useful for giving more than one user
|
||||||
|
access to sshuttle. The default is `sshuttle_auto`
|
||||||
|
|
||||||
|
sshuttle --sudoer --sudoers-filename sshuttle_auto_mike
|
||||||
|
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
|
||||||
|
|
||||||
|
You can also see what configuration will be added to your system without
|
||||||
|
modifying anything. This can be helpfull is the auto feature does not work, or
|
||||||
|
you want more control. This option also works with `--sudoers-username`.
|
||||||
|
`--sudoers-filename` has no effect with this option.
|
||||||
|
|
||||||
sshuttle --sudoers-no-modify
|
sshuttle --sudoers-no-modify
|
||||||
|
|
||||||
A custom user or group can be set with the
|
This will simply sprint the generated configuration to STDOUT. Example
|
||||||
: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`::
|
|
||||||
|
|
||||||
sshuttle --sudoers-no-modify --sudoers-user mike
|
08:40 PM william$ sshuttle --sudoers-no-modify
|
||||||
sshuttle --sudoers-no-modify --sudoers-user %sudo
|
|
||||||
|
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
|
||||||
|
@ -1,16 +1,7 @@
|
|||||||
Microsoft Windows
|
Microsoft Windows
|
||||||
=================
|
=================
|
||||||
|
Currently there is no built in support for running sshuttle directly on
|
||||||
Experimental native support::
|
Microsoft Windows.
|
||||||
|
|
||||||
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
|
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
|
Virtualbox if you like). In the Vagrant settings, remember to turn on bridged
|
||||||
@ -25,4 +16,4 @@ Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can
|
|||||||
configure that in Vagrant), we can then ask Windows to route all its traffic
|
configure that in Vagrant), we can then ask Windows to route all its traffic
|
||||||
via the VM by running the following in cmd.exe with admin right::
|
via the VM by running the following in cmd.exe with admin right::
|
||||||
|
|
||||||
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200
|
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200
|
||||||
|
133
flake.lock
generated
133
flake.lock
generated
@ -1,133 +0,0 @@
|
|||||||
{
|
|
||||||
"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
117
flake.nix
@ -1,117 +0,0 @@
|
|||||||
{
|
|
||||||
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
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
[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"
|
|
||||||
]
|
|
7
requirements-tests.txt
Normal file
7
requirements-tests.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
attrs==19.3.0
|
||||||
|
pytest==5.4.3
|
||||||
|
pytest-cov==2.9.0
|
||||||
|
mock==2.0.0
|
||||||
|
flake8==3.8.2
|
||||||
|
pyflakes==2.2.0
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
setuptools-scm==4.1.2
|
4
run
4
run
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
set -e
|
set -e
|
||||||
export PYTHONPATH="$(dirname "$0"):$PYTHONPATH"
|
export PYTHONPATH="$(dirname $0):$PYTHONPATH"
|
||||||
export PATH="$(dirname "$0")/bin:$PATH"
|
export PATH="$(dirname $0)/bin:$PATH"
|
||||||
|
|
||||||
python_best_version() {
|
python_best_version() {
|
||||||
if [ -x "$(command -v python3)" ] &&
|
if [ -x "$(command -v python3)" ] &&
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
# 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
|
|
@ -1,21 +0,0 @@
|
|||||||
# 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>
|
|
@ -1,34 +0,0 @@
|
|||||||
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
|
|
@ -1,65 +0,0 @@
|
|||||||
#!/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 &
|
|
@ -1,159 +0,0 @@
|
|||||||
#!/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[@]}"
|
|
@ -1,86 +0,0 @@
|
|||||||
#!/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
|
|
@ -1,40 +0,0 @@
|
|||||||
#!/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
|
|
@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
export PYTHONPATH=.
|
|
||||||
|
|
||||||
set -x
|
|
||||||
python -m flake8 sshuttle tests
|
|
||||||
python -m pytest .
|
|
@ -1,42 +0,0 @@
|
|||||||
#!/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
|
|
25
setup.cfg
25
setup.cfg
@ -1,30 +1,17 @@
|
|||||||
[bumpversion]
|
|
||||||
current_version = 1.3.1
|
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
|
||||||
|
|
||||||
[bumpversion:file:pyproject.toml]
|
|
||||||
|
|
||||||
[bumpversion:file:sshuttle/version.py]
|
|
||||||
|
|
||||||
[aliases]
|
[aliases]
|
||||||
test = pytest
|
test=pytest
|
||||||
|
|
||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
|
||||||
[upload]
|
[upload]
|
||||||
sign = true
|
sign=true
|
||||||
identity = 0x1784577F811F6EAC
|
identity=0x1784577F811F6EAC
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
count = true
|
count=true
|
||||||
show-source = true
|
show-source=true
|
||||||
statistics = true
|
statistics=true
|
||||||
max-line-length = 128
|
|
||||||
|
|
||||||
[pycodestyle]
|
|
||||||
max-line-length = 128
|
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
addopts = --cov=sshuttle --cov-branch --cov-report=term-missing
|
addopts = --cov=sshuttle --cov-branch --cov-report=term-missing
|
||||||
|
72
setup.py
Executable file
72
setup.py
Executable file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright 2012-2014 Brian May
|
||||||
|
#
|
||||||
|
# This file is part of sshuttle.
|
||||||
|
#
|
||||||
|
# sshuttle is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as
|
||||||
|
# published by the Free Software Foundation; either version 2.1 of
|
||||||
|
# the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# sshuttle is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with sshuttle; If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
def version_scheme(version):
|
||||||
|
from setuptools_scm.version import guess_next_dev_version
|
||||||
|
version = guess_next_dev_version(version)
|
||||||
|
return version.lstrip("v")
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="sshuttle",
|
||||||
|
use_scm_version={
|
||||||
|
'write_to': "sshuttle/version.py",
|
||||||
|
'version_scheme': version_scheme,
|
||||||
|
},
|
||||||
|
setup_requires=['setuptools_scm'],
|
||||||
|
# version=version,
|
||||||
|
url='https://github.com/sshuttle/sshuttle',
|
||||||
|
author='Brian May',
|
||||||
|
author_email='brian@linuxpenguins.xyz',
|
||||||
|
description='Full-featured" VPN over an SSH tunnel',
|
||||||
|
packages=find_packages(),
|
||||||
|
license="LGPL2.1+",
|
||||||
|
long_description=open('README.rst').read(),
|
||||||
|
long_description_content_type="text/x-rst",
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: End Users/Desktop",
|
||||||
|
"License :: OSI Approved :: "
|
||||||
|
"GNU Lesser General Public License v2 or later (LGPLv2+)",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Topic :: System :: Networking",
|
||||||
|
],
|
||||||
|
scripts=['bin/sudoers-add'],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'sshuttle = sshuttle.cmdline:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tests_require=[
|
||||||
|
'pytest',
|
||||||
|
'pytest-cov',
|
||||||
|
'pytest-runner',
|
||||||
|
'mock',
|
||||||
|
'flake8',
|
||||||
|
],
|
||||||
|
keywords="ssh vpn",
|
||||||
|
)
|
@ -1 +1,4 @@
|
|||||||
__version__ = "1.3.1"
|
try:
|
||||||
|
from sshuttle.version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = "unknown"
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
"""Coverage.py's main entry point."""
|
"""Coverage.py's main entry point."""
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
from sshuttle.cmdline import main
|
from sshuttle.cmdline import main
|
||||||
from sshuttle.helpers import debug3
|
sys.exit(main())
|
||||||
|
|
||||||
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)
|
|
||||||
|
@ -1,31 +1,21 @@
|
|||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
import types
|
import imp
|
||||||
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()
|
z = zlib.decompressobj()
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
name = stdin.readline().strip()
|
name = sys.stdin.readline().strip()
|
||||||
if name:
|
if name:
|
||||||
# python2 compat: in python2 stdin.readline().strip() -> str
|
name = name.decode("ASCII")
|
||||||
# in python3 stdin.readline().strip() -> bytes
|
|
||||||
# (see #481)
|
|
||||||
if sys.version_info >= (3, 0):
|
|
||||||
name = name.decode("ASCII")
|
|
||||||
nbytes = int(stdin.readline())
|
|
||||||
if verbosity >= 2:
|
|
||||||
sys.stderr.write(' s: assembling %r (%d bytes)\n'
|
|
||||||
% (name, nbytes))
|
|
||||||
content = z.decompress(stdin.read(nbytes))
|
|
||||||
|
|
||||||
module = types.ModuleType(name)
|
nbytes = int(sys.stdin.readline())
|
||||||
|
if verbosity >= 2:
|
||||||
|
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
||||||
|
% (name, nbytes))
|
||||||
|
content = z.decompress(sys.stdin.read(nbytes))
|
||||||
|
|
||||||
|
module = imp.new_module(name)
|
||||||
parents = name.rsplit(".", 1)
|
parents = name.rsplit(".", 1)
|
||||||
if len(parents) == 2:
|
if len(parents) == 2:
|
||||||
parent, parent_name = parents
|
parent, parent_name = parents
|
||||||
@ -47,7 +37,5 @@ sshuttle.helpers.verbose = verbosity
|
|||||||
|
|
||||||
import sshuttle.cmdline_options as options # noqa: E402
|
import sshuttle.cmdline_options as options # noqa: E402
|
||||||
from sshuttle.server import main # noqa: E402
|
from sshuttle.server import main # noqa: E402
|
||||||
|
main(options.latency_control, options.auto_hosts, options.to_nameserver,
|
||||||
main(options.latency_control, options.latency_buffer_size,
|
|
||||||
options.auto_hosts, options.to_nameserver,
|
|
||||||
options.auto_nets)
|
options.auto_nets)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,6 @@
|
|||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import shlex
|
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import platform
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
import sshuttle.client as client
|
import sshuttle.client as client
|
||||||
import sshuttle.firewall as firewall
|
import sshuttle.firewall as firewall
|
||||||
@ -11,21 +9,25 @@ import sshuttle.ssyslog as ssyslog
|
|||||||
from sshuttle.options import parser, parse_ipport
|
from sshuttle.options import parser, parse_ipport
|
||||||
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
||||||
from sshuttle.sudoers import sudoers
|
from sshuttle.sudoers import sudoers
|
||||||
from sshuttle.namespace import enter_namespace
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if 'SSHUTTLE_ARGS' in os.environ:
|
opt = parser.parse_args()
|
||||||
env_args = shlex.split(os.environ['SSHUTTLE_ARGS'])
|
|
||||||
else:
|
|
||||||
env_args = []
|
|
||||||
args = [*env_args, *sys.argv[1:]]
|
|
||||||
|
|
||||||
opt = parser.parse_args(args)
|
if opt.sudoers or opt.sudoers_no_modify:
|
||||||
|
if platform.platform().startswith('OpenBSD'):
|
||||||
|
log('Automatic sudoers does not work on BSD')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
if opt.sudoers_no_modify:
|
if not opt.sudoers_filename:
|
||||||
# sudoers() calls exit() when it completes
|
log('--sudoers-file must be set or omited.')
|
||||||
sudoers(user_name=opt.sudoers_user)
|
exit(1)
|
||||||
|
|
||||||
|
sudoers(
|
||||||
|
user_name=opt.sudoers_user,
|
||||||
|
no_modify=opt.sudoers_no_modify,
|
||||||
|
file_name=opt.sudoers_filename
|
||||||
|
)
|
||||||
|
|
||||||
if opt.daemon:
|
if opt.daemon:
|
||||||
opt.syslog = 1
|
opt.syslog = 1
|
||||||
@ -38,34 +40,15 @@ def main():
|
|||||||
helpers.verbose = opt.verbose
|
helpers.verbose = opt.verbose
|
||||||
|
|
||||||
try:
|
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.firewall:
|
||||||
if opt.subnets or opt.subnets_file:
|
if opt.subnets or opt.subnets_file:
|
||||||
parser.error('exactly zero arguments expected')
|
parser.error('exactly zero arguments expected')
|
||||||
return firewall.main(opt.method, opt.syslog)
|
return firewall.main(opt.method, opt.syslog)
|
||||||
elif opt.hostwatch:
|
elif opt.hostwatch:
|
||||||
hostwatch.hw_main(opt.subnets, opt.auto_hosts)
|
return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
|
||||||
return 0
|
|
||||||
else:
|
else:
|
||||||
# parse_subnetports() is used to create a list of includes
|
includes = opt.subnets + opt.subnets_file
|
||||||
# and excludes. It is called once for each parameter and
|
excludes = opt.exclude
|
||||||
# returns a list of one or more items for each subnet (it
|
|
||||||
# can return more than one item when a hostname in the
|
|
||||||
# parameter resolves to multiple IP addresses. Here, we
|
|
||||||
# flatten these lists.
|
|
||||||
includes = [item for sublist in opt.subnets+opt.subnets_file
|
|
||||||
for item in sublist]
|
|
||||||
excludes = [item for sublist in opt.exclude for item in sublist]
|
|
||||||
|
|
||||||
if not includes and not opt.auto_nets:
|
if not includes and not opt.auto_nets:
|
||||||
parser.error('at least one subnet, subnet file, '
|
parser.error('at least one subnet, subnet file, '
|
||||||
'or -N expected')
|
'or -N expected')
|
||||||
@ -94,13 +77,6 @@ def main():
|
|||||||
ipport_v4 = "auto"
|
ipport_v4 = "auto"
|
||||||
# parse_ipport6('[::1]:0')
|
# parse_ipport6('[::1]:0')
|
||||||
ipport_v6 = "auto" if not opt.disable_ipv6 else None
|
ipport_v6 = "auto" if not opt.disable_ipv6 else None
|
||||||
try:
|
|
||||||
int(opt.tmark, 16)
|
|
||||||
except ValueError:
|
|
||||||
parser.error("--tmark must be a hexadecimal value")
|
|
||||||
opt.tmark = opt.tmark.lower() # make 'x' in 0x lowercase
|
|
||||||
if not opt.tmark.startswith("0x"): # accept without 0x prefix
|
|
||||||
opt.tmark = "0x%s" % opt.tmark
|
|
||||||
if opt.syslog:
|
if opt.syslog:
|
||||||
ssyslog.start_syslog()
|
ssyslog.start_syslog()
|
||||||
ssyslog.close_stdin()
|
ssyslog.close_stdin()
|
||||||
@ -111,7 +87,6 @@ def main():
|
|||||||
remotename,
|
remotename,
|
||||||
opt.python,
|
opt.python,
|
||||||
opt.latency_control,
|
opt.latency_control,
|
||||||
opt.latency_buffer_size,
|
|
||||||
opt.dns,
|
opt.dns,
|
||||||
nslist,
|
nslist,
|
||||||
opt.method,
|
opt.method,
|
||||||
@ -124,11 +99,7 @@ def main():
|
|||||||
opt.to_ns,
|
opt.to_ns,
|
||||||
opt.pidfile,
|
opt.pidfile,
|
||||||
opt.user,
|
opt.user,
|
||||||
opt.group,
|
opt.sudo_pythonpath)
|
||||||
opt.sudo_pythonpath,
|
|
||||||
opt.add_cmd_delimiter,
|
|
||||||
opt.remote_shell,
|
|
||||||
opt.tmark)
|
|
||||||
|
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
log('Normal exit code, exiting...')
|
log('Normal exit code, exiting...')
|
||||||
@ -137,9 +108,9 @@ def main():
|
|||||||
return return_code
|
return return_code
|
||||||
|
|
||||||
except Fatal as e:
|
except Fatal as e:
|
||||||
log('fatal: %s' % e)
|
log('fatal: %s\n' % e)
|
||||||
return 99
|
return 99
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log('\n')
|
log('\n')
|
||||||
log('Keyboard interrupt: exiting.')
|
log('Keyboard interrupt: exiting.\n')
|
||||||
return 1
|
return 1
|
||||||
|
@ -1,25 +1,16 @@
|
|||||||
import errno
|
import errno
|
||||||
import shutil
|
|
||||||
import socket
|
import socket
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import traceback
|
import traceback
|
||||||
import subprocess as ssubprocess
|
|
||||||
import base64
|
|
||||||
import io
|
|
||||||
|
|
||||||
import sshuttle.ssyslog as ssyslog
|
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
|
from sshuttle.methods import get_auto_method, get_method
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
HOSTSFILE = '/etc/hosts'
|
||||||
HOSTSFILE = r"C:\Windows\System32\drivers\etc\hosts"
|
|
||||||
else:
|
|
||||||
HOSTSFILE = '/etc/hosts'
|
|
||||||
sshuttle_pid = None
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_etc_hosts(hostmap, port):
|
def rewrite_etc_hosts(hostmap, port):
|
||||||
@ -36,11 +27,7 @@ def rewrite_etc_hosts(hostmap, port):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
if old_content.strip() and not os.path.exists(BAKFILE):
|
if old_content.strip() and not os.path.exists(BAKFILE):
|
||||||
try:
|
os.link(HOSTSFILE, BAKFILE)
|
||||||
os.link(HOSTSFILE, BAKFILE)
|
|
||||||
except OSError:
|
|
||||||
# file is locked - performing non-atomic copy
|
|
||||||
shutil.copyfile(HOSTSFILE, BAKFILE)
|
|
||||||
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
||||||
f = open(tmpname, 'w')
|
f = open(tmpname, 'w')
|
||||||
for line in old_content.rstrip().split('\n'):
|
for line in old_content.rstrip().split('\n'):
|
||||||
@ -51,102 +38,41 @@ def rewrite_etc_hosts(hostmap, port):
|
|||||||
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
|
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
if sys.platform != 'win32':
|
if st is not None:
|
||||||
if st is not None:
|
os.chown(tmpname, st.st_uid, st.st_gid)
|
||||||
os.chown(tmpname, st.st_uid, st.st_gid)
|
os.chmod(tmpname, st.st_mode)
|
||||||
os.chmod(tmpname, st.st_mode)
|
else:
|
||||||
else:
|
os.chown(tmpname, 0, 0)
|
||||||
os.chown(tmpname, 0, 0)
|
os.chmod(tmpname, 0o600)
|
||||||
os.chmod(tmpname, 0o644)
|
os.rename(tmpname, HOSTSFILE)
|
||||||
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):
|
def restore_etc_hosts(port):
|
||||||
# Only restore if we added hosts to /etc/hosts previously.
|
rewrite_etc_hosts({}, port)
|
||||||
if len(hostmap) > 0:
|
|
||||||
debug2('undoing /etc/hosts changes.')
|
|
||||||
rewrite_etc_hosts({}, port)
|
|
||||||
|
|
||||||
|
|
||||||
def firewall_exit(signum, frame):
|
# Isolate function that needs to be replaced for tests
|
||||||
# The typical sshuttle exit is that the main sshuttle process
|
def setup_daemon():
|
||||||
# exits, closes file descriptors it uses, and the firewall process
|
if os.getuid() != 0:
|
||||||
# notices that it can't read from stdin anymore and exits
|
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
||||||
# (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
|
# don't disappear if our controlling terminal or stdout/stderr
|
||||||
# disappears; we still have to clean up.
|
# disappears; we still have to clean up.
|
||||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||||
signal.signal(signal.SIGTERM, firewall_exit)
|
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||||
signal.signal(signal.SIGINT, firewall_exit)
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
|
||||||
# Calling setsid() here isn't strictly necessary. However, it forces
|
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
||||||
# Ctrl+C to get sent to the main sshuttle process instead of to
|
# I'll die automatically.
|
||||||
# the firewall process---which is our preferred way to shutdown.
|
os.setsid()
|
||||||
# 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
|
|
||||||
|
|
||||||
return sys.stdin.buffer, sys.stdout.buffer
|
# because of limitations of the 'su' command, the *real* stdin/stdout
|
||||||
|
# are both attached to stdout initially. Clone stdout into stdin so we
|
||||||
|
# can read from it.
|
||||||
|
os.dup2(1, 0)
|
||||||
|
|
||||||
|
return sys.stdin, sys.stdout
|
||||||
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:
|
# Note that we're sorting in a very particular order:
|
||||||
@ -159,50 +85,19 @@ def subnet_weight(s):
|
|||||||
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
|
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
|
||||||
|
|
||||||
|
|
||||||
def flush_systemd_dns_cache():
|
|
||||||
# If the user is using systemd-resolve for DNS resolution, it is
|
|
||||||
# possible for the request to go through systemd-resolve before we
|
|
||||||
# see it...and it may use a cached result instead of sending a
|
|
||||||
# request that we can intercept. When sshuttle starts and stops,
|
|
||||||
# this means that we should clear the cache!
|
|
||||||
#
|
|
||||||
# The command to do this was named systemd-resolve, but changed to
|
|
||||||
# resolvectl in systemd 239.
|
|
||||||
# https://github.com/systemd/systemd/blob/f8eb41003df1a4eab59ff9bec67b2787c9368dbd/NEWS#L3816
|
|
||||||
|
|
||||||
p = None
|
|
||||||
if helpers.which("resolvectl"):
|
|
||||||
debug2("Flushing systemd's DNS resolver cache: "
|
|
||||||
"resolvectl flush-caches")
|
|
||||||
p = ssubprocess.Popen(["resolvectl", "flush-caches"],
|
|
||||||
stdout=ssubprocess.PIPE, env=helpers.get_env())
|
|
||||||
elif helpers.which("systemd-resolve"):
|
|
||||||
debug2("Flushing systemd's DNS resolver cache: "
|
|
||||||
"systemd-resolve --flush-caches")
|
|
||||||
p = ssubprocess.Popen(["systemd-resolve", "--flush-caches"],
|
|
||||||
stdout=ssubprocess.PIPE, env=helpers.get_env())
|
|
||||||
|
|
||||||
if p:
|
|
||||||
# Wait so flush is finished and process doesn't show up as defunct.
|
|
||||||
rv = p.wait()
|
|
||||||
if rv != 0:
|
|
||||||
log("Received non-zero return code %d when flushing DNS resolver "
|
|
||||||
"cache." % rv)
|
|
||||||
|
|
||||||
|
|
||||||
# This is some voodoo for setting up the kernel's transparent
|
# This is some voodoo for setting up the kernel's transparent
|
||||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||||
# otherwise we delete it, then make them from scratch.
|
# otherwise we delete it, then make them from scratch.
|
||||||
#
|
#
|
||||||
# This code is supposed to clean up after itself by deleting its rules on
|
# This code is supposed to clean up after itself by deleting its rules on
|
||||||
# exit. In case that fails, it's not the end of the world; future runs will
|
# exit. In case that fails, it's not the end of the world; future runs will
|
||||||
# supersede it in the transproxy list, at least, so the leftover rules
|
# supercede it in the transproxy list, at least, so the leftover rules
|
||||||
# are hopefully harmless.
|
# are hopefully harmless.
|
||||||
def main(method_name, syslog):
|
def main(method_name, syslog):
|
||||||
helpers.logprefix = 'fw: '
|
|
||||||
stdin, stdout = setup_daemon()
|
stdin, stdout = setup_daemon()
|
||||||
hostmap = {}
|
hostmap = {}
|
||||||
debug1('Starting firewall with Python version %s'
|
|
||||||
|
debug1('firewall manager: Starting firewall with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
if method_name == "auto":
|
if method_name == "auto":
|
||||||
@ -214,50 +109,31 @@ def main(method_name, syslog):
|
|||||||
ssyslog.start_syslog()
|
ssyslog.start_syslog()
|
||||||
ssyslog.stderr_to_syslog()
|
ssyslog.stderr_to_syslog()
|
||||||
|
|
||||||
if not method.is_supported():
|
debug1('firewall manager: ready method name %s.\n' % method.name)
|
||||||
raise Fatal("The %s method is not supported on this machine. "
|
stdout.write('READY %s\n' % method.name)
|
||||||
"Check that the appropriate programs are in your "
|
|
||||||
"PATH." % method_name)
|
|
||||||
|
|
||||||
debug1('ready method name %s.' % method.name)
|
|
||||||
stdout.write(('READY %s\n' % method.name).encode('ASCII'))
|
|
||||||
stdout.flush()
|
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,
|
# 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
|
# sshuttle can launch us as early as possible (and get sudo password
|
||||||
# authentication as early in the startup process as possible).
|
# authentication as early in the startup process as possible).
|
||||||
try:
|
line = stdin.readline(128)
|
||||||
line = _read_next_string_line()
|
if not line:
|
||||||
if not line:
|
return # parent died; nothing to do
|
||||||
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 = []
|
subnets = []
|
||||||
if line != 'ROUTES':
|
if line != 'ROUTES\n':
|
||||||
raise Fatal('expected ROUTES but got %r' % line)
|
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
||||||
while 1:
|
while 1:
|
||||||
line = _read_next_string_line()
|
line = stdin.readline(128)
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('expected route but got %r' % line)
|
raise Fatal('firewall: expected route but got %r' % line)
|
||||||
elif line.startswith("NSLIST"):
|
elif line.startswith("NSLIST\n"):
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, width, exclude, ip, fport, lport) = line.split(',', 5)
|
(family, width, exclude, ip, fport, lport) = \
|
||||||
except Exception:
|
line.strip().split(',', 5)
|
||||||
raise Fatal('expected route or NSLIST but got %r' % line)
|
except BaseException:
|
||||||
|
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
|
||||||
subnets.append((
|
subnets.append((
|
||||||
int(family),
|
int(family),
|
||||||
int(width),
|
int(width),
|
||||||
@ -265,63 +141,60 @@ def main(method_name, syslog):
|
|||||||
ip,
|
ip,
|
||||||
int(fport),
|
int(fport),
|
||||||
int(lport)))
|
int(lport)))
|
||||||
debug2('Got subnets: %r' % subnets)
|
debug2('firewall manager: Got subnets: %r\n' % subnets)
|
||||||
|
|
||||||
nslist = []
|
nslist = []
|
||||||
if line != 'NSLIST':
|
if line != 'NSLIST\n':
|
||||||
raise Fatal('expected NSLIST but got %r' % line)
|
raise Fatal('firewall: expected NSLIST but got %r' % line)
|
||||||
while 1:
|
while 1:
|
||||||
line = _read_next_string_line()
|
line = stdin.readline(128)
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('expected nslist but got %r' % line)
|
raise Fatal('firewall: expected nslist but got %r' % line)
|
||||||
elif line.startswith("PORTS "):
|
elif line.startswith("PORTS "):
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, ip) = line.split(',', 1)
|
(family, ip) = line.strip().split(',', 1)
|
||||||
except Exception:
|
except BaseException:
|
||||||
raise Fatal('expected nslist or PORTS but got %r' % line)
|
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
|
||||||
nslist.append((int(family), ip))
|
nslist.append((int(family), ip))
|
||||||
debug2('Got partial nslist: %r' % nslist)
|
debug2('firewall manager: Got partial nslist: %r\n' % nslist)
|
||||||
debug2('Got nslist: %r' % nslist)
|
debug2('firewall manager: Got nslist: %r\n' % nslist)
|
||||||
|
|
||||||
if not line.startswith('PORTS '):
|
if not line.startswith('PORTS '):
|
||||||
raise Fatal('expected PORTS but got %r' % line)
|
raise Fatal('firewall: expected PORTS but got %r' % line)
|
||||||
_, _, ports = line.partition(" ")
|
_, _, ports = line.partition(" ")
|
||||||
ports = ports.split(",")
|
ports = ports.split(",")
|
||||||
if len(ports) != 4:
|
if len(ports) != 4:
|
||||||
raise Fatal('expected 4 ports but got %d' % len(ports))
|
raise Fatal('firewall: expected 4 ports but got %d' % len(ports))
|
||||||
port_v6 = int(ports[0])
|
port_v6 = int(ports[0])
|
||||||
port_v4 = int(ports[1])
|
port_v4 = int(ports[1])
|
||||||
dnsport_v6 = int(ports[2])
|
dnsport_v6 = int(ports[2])
|
||||||
dnsport_v4 = int(ports[3])
|
dnsport_v4 = int(ports[3])
|
||||||
|
|
||||||
assert port_v6 >= 0
|
assert(port_v6 >= 0)
|
||||||
assert port_v6 <= 65535
|
assert(port_v6 <= 65535)
|
||||||
assert port_v4 >= 0
|
assert(port_v4 >= 0)
|
||||||
assert port_v4 <= 65535
|
assert(port_v4 <= 65535)
|
||||||
assert dnsport_v6 >= 0
|
assert(dnsport_v6 >= 0)
|
||||||
assert dnsport_v6 <= 65535
|
assert(dnsport_v6 <= 65535)
|
||||||
assert dnsport_v4 >= 0
|
assert(dnsport_v4 >= 0)
|
||||||
assert dnsport_v4 <= 65535
|
assert(dnsport_v4 <= 65535)
|
||||||
|
|
||||||
debug2('Got ports: %d,%d,%d,%d'
|
debug2('firewall manager: Got ports: %d,%d,%d,%d\n'
|
||||||
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
||||||
|
|
||||||
line = _read_next_string_line()
|
line = stdin.readline(128)
|
||||||
if not line or not line.startswith("GO "):
|
if not line:
|
||||||
raise Fatal('expected GO but got %r' % line)
|
raise Fatal('firewall: expected GO but got %r' % line)
|
||||||
|
elif not line.startswith("GO "):
|
||||||
|
raise Fatal('firewall: expected GO but got %r' % line)
|
||||||
|
|
||||||
_, _, args = line.partition(" ")
|
_, _, args = line.partition(" ")
|
||||||
global sshuttle_pid
|
udp, user = args.strip().split(" ", 1)
|
||||||
udp, user, group, tmark, sshuttle_pid = args.split(" ", 4)
|
|
||||||
udp = bool(int(udp))
|
udp = bool(int(udp))
|
||||||
sshuttle_pid = int(sshuttle_pid)
|
|
||||||
if user == '-':
|
if user == '-':
|
||||||
user = None
|
user = None
|
||||||
if group == '-':
|
debug2('firewall manager: Got udp: %r, user: %r\n' % (udp, user))
|
||||||
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]
|
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
||||||
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
||||||
@ -329,100 +202,86 @@ def main(method_name, syslog):
|
|||||||
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
debug1('setting up.')
|
debug1('firewall manager: setting up.\n')
|
||||||
|
|
||||||
if subnets_v6 or nslist_v6:
|
if subnets_v6 or nslist_v6:
|
||||||
debug2('setting up IPv6.')
|
debug2('firewall manager: setting up IPv6.\n')
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
port_v6, dnsport_v6, nslist_v6,
|
port_v6, dnsport_v6, nslist_v6,
|
||||||
socket.AF_INET6, subnets_v6, udp,
|
socket.AF_INET6, subnets_v6, udp,
|
||||||
user, group, tmark)
|
user)
|
||||||
|
|
||||||
if subnets_v4 or nslist_v4:
|
if subnets_v4 or nslist_v4:
|
||||||
debug2('setting up IPv4.')
|
debug2('firewall manager: setting up IPv4.\n')
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
port_v4, dnsport_v4, nslist_v4,
|
port_v4, dnsport_v4, nslist_v4,
|
||||||
socket.AF_INET, subnets_v4, udp,
|
socket.AF_INET, subnets_v4, udp,
|
||||||
user, group, tmark)
|
user)
|
||||||
|
|
||||||
|
stdout.write('STARTED\n')
|
||||||
|
|
||||||
try:
|
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()
|
stdout.flush()
|
||||||
except IOError as e: # the parent process probably died
|
except IOError:
|
||||||
debug3('write to stdout failed: %s' % (e,))
|
# the parent process died for some reason; he's surely been loud
|
||||||
|
# enough, so no reason to report another error
|
||||||
return
|
return
|
||||||
|
|
||||||
# Now we wait until EOF or any other kind of exception. We need
|
# 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
|
# to stay running so that we don't need a *second* password
|
||||||
# authentication at shutdown time - that cleanup is important!
|
# authentication at shutdown time - that cleanup is important!
|
||||||
while 1:
|
while 1:
|
||||||
line = _read_next_string_line()
|
line = stdin.readline(128)
|
||||||
if not line:
|
|
||||||
return
|
|
||||||
if line.startswith('HOST '):
|
if line.startswith('HOST '):
|
||||||
(name, ip) = line[5:].split(',', 1)
|
(name, ip) = line[5:].strip().split(',', 1)
|
||||||
hostmap[name] = ip
|
hostmap[name] = ip
|
||||||
debug2('setting up /etc/hosts.')
|
debug2('firewall manager: setting up /etc/hosts.\n')
|
||||||
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
|
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
|
||||||
elif line:
|
elif line:
|
||||||
if not method.firewall_command(line):
|
if not method.firewall_command(line):
|
||||||
raise Fatal('expected command, got %r' % line)
|
raise Fatal('firewall: expected command, got %r' % line)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
debug1('undoing changes.')
|
debug1('firewall manager: undoing changes.\n')
|
||||||
except Exception:
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if subnets_v6 or nslist_v6:
|
if subnets_v6 or nslist_v6:
|
||||||
debug2('undoing IPv6 changes.')
|
debug2('firewall manager: undoing IPv6 changes.\n')
|
||||||
method.restore_firewall(port_v6, socket.AF_INET6, udp, user, group)
|
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
|
||||||
except Exception:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("Error trying to undo IPv6 firewall.")
|
debug1("firewall manager: "
|
||||||
debug1(traceback.format_exc())
|
"Error trying to undo IPv6 firewall.\n")
|
||||||
except Exception:
|
for line in traceback.format_exc().splitlines():
|
||||||
|
debug1("---> %s\n" % line)
|
||||||
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if subnets_v4 or nslist_v4:
|
if subnets_v4 or nslist_v4:
|
||||||
debug2('undoing IPv4 changes.')
|
debug2('firewall manager: undoing IPv4 changes.\n')
|
||||||
method.restore_firewall(port_v4, socket.AF_INET, udp, user, group)
|
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
|
||||||
except Exception:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("Error trying to undo IPv4 firewall.")
|
debug1("firewall manager: "
|
||||||
debug1(traceback.format_exc())
|
"Error trying to undo IPv4 firewall.\n")
|
||||||
except Exception:
|
for line in traceback.format_exc().splitlines():
|
||||||
|
debug1("firewall manager: ---> %s\n" % line)
|
||||||
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# debug2() message printed in restore_etc_hosts() function.
|
debug2('firewall manager: undoing /etc/hosts changes.\n')
|
||||||
restore_etc_hosts(hostmap, port_v6 or port_v4)
|
restore_etc_hosts(port_v6 or port_v4)
|
||||||
except Exception:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("Error trying to undo /etc/hosts changes.")
|
debug1("firewall manager: "
|
||||||
debug1(traceback.format_exc())
|
"Error trying to undo /etc/hosts changes.\n")
|
||||||
except Exception:
|
for line in traceback.format_exc().splitlines():
|
||||||
|
debug1("firewall manager: ---> %s\n" % line)
|
||||||
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
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.")
|
|
||||||
|
@ -1,14 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
import errno
|
import errno
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import subprocess
|
|
||||||
import traceback
|
|
||||||
import re
|
|
||||||
|
|
||||||
if sys.platform != "win32":
|
|
||||||
import fcntl
|
|
||||||
|
|
||||||
logprefix = ''
|
logprefix = ''
|
||||||
verbose = 0
|
verbose = 0
|
||||||
@ -18,28 +10,20 @@ def b(s):
|
|||||||
return s.encode("ASCII")
|
return s.encode("ASCII")
|
||||||
|
|
||||||
|
|
||||||
def get_verbose_level():
|
|
||||||
return verbose
|
|
||||||
|
|
||||||
|
|
||||||
def log(s):
|
def log(s):
|
||||||
global logprefix
|
global logprefix
|
||||||
try:
|
try:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except (IOError, ValueError): # ValueError ~ I/O operation on closed file
|
if s.find("\n") != -1:
|
||||||
pass
|
prefix = logprefix
|
||||||
try:
|
s = s.rstrip("\n")
|
||||||
# Put newline at end of string if line doesn't have one.
|
for line in s.split("\n"):
|
||||||
if not s.endswith("\n"):
|
sys.stderr.write(prefix + line + "\n")
|
||||||
s = s+"\n"
|
prefix = "---> "
|
||||||
|
else:
|
||||||
prefix = logprefix
|
sys.stderr.write(logprefix + s)
|
||||||
s = s.rstrip("\n")
|
|
||||||
for line in s.split("\n"):
|
|
||||||
sys.stderr.write(prefix + line + "\n")
|
|
||||||
prefix = " "
|
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
except (IOError, ValueError): # ValueError ~ I/O operation on closed file
|
except IOError:
|
||||||
# this could happen if stderr gets forcibly disconnected, eg. because
|
# this could happen if stderr gets forcibly disconnected, eg. because
|
||||||
# our tty closes. That sucks, but it's no reason to abort the program.
|
# our tty closes. That sucks, but it's no reason to abort the program.
|
||||||
pass
|
pass
|
||||||
@ -64,95 +48,23 @@ class Fatal(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def resolvconf_nameservers(systemd_resolved):
|
def resolvconf_nameservers():
|
||||||
"""Retrieves a list of tuples (address type, address as a string) of
|
lines = []
|
||||||
the DNS servers used by the system to resolve hostnames.
|
for line in open('/etc/resolv.conf'):
|
||||||
|
words = line.lower().split()
|
||||||
If parameter is False, DNS servers are retrieved from only
|
if len(words) >= 2 and words[0] == 'nameserver':
|
||||||
/etc/resolv.conf. This behavior makes sense for the sshuttle
|
lines.append(family_ip_tuple(words[1]))
|
||||||
server.
|
return lines
|
||||||
|
|
||||||
If parameter is True, we retrieve information from both
|
|
||||||
/etc/resolv.conf and /run/systemd/resolve/resolv.conf (if it
|
|
||||||
exists). This behavior makes sense for the sshuttle client.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Historically, we just needed to read /etc/resolv.conf.
|
|
||||||
#
|
|
||||||
# If systemd-resolved is active, /etc/resolv.conf will point to
|
|
||||||
# localhost and the actual DNS servers that systemd-resolved uses
|
|
||||||
# are stored in /run/systemd/resolve/resolv.conf. For programs
|
|
||||||
# that use the localhost DNS server, having sshuttle read
|
|
||||||
# /etc/resolv.conf is sufficient. However, resolved provides other
|
|
||||||
# ways of resolving hostnames (such as via dbus) that may not
|
|
||||||
# route requests through localhost. So, we retrieve a list of DNS
|
|
||||||
# servers that resolved uses so we can intercept those as well.
|
|
||||||
#
|
|
||||||
# For more information about systemd-resolved, see:
|
|
||||||
# https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html
|
|
||||||
#
|
|
||||||
# On machines without systemd-resolved, we expect opening the
|
|
||||||
# second file will fail.
|
|
||||||
files = ['/etc/resolv.conf']
|
|
||||||
if systemd_resolved:
|
|
||||||
files += ['/run/systemd/resolve/resolv.conf']
|
|
||||||
|
|
||||||
nsservers = []
|
|
||||||
for f in files:
|
|
||||||
this_file_nsservers = []
|
|
||||||
try:
|
|
||||||
for line in open(f):
|
|
||||||
words = line.lower().split()
|
|
||||||
if len(words) >= 2 and words[0] == 'nameserver':
|
|
||||||
this_file_nsservers.append(family_ip_tuple(words[1]))
|
|
||||||
debug2("Found DNS servers in %s: %s" %
|
|
||||||
(f, [n[1] for n in this_file_nsservers]))
|
|
||||||
nsservers += this_file_nsservers
|
|
||||||
except OSError as e:
|
|
||||||
debug3("Failed to read %s when looking for DNS servers: %s" %
|
|
||||||
(f, e.strerror))
|
|
||||||
|
|
||||||
return nsservers
|
|
||||||
|
|
||||||
|
|
||||||
def windows_nameservers():
|
def resolvconf_random_nameserver():
|
||||||
out = subprocess.check_output(["powershell", "-NonInteractive", "-NoProfile", "-Command", "Get-DnsClientServerAddress"],
|
lines = resolvconf_nameservers()
|
||||||
encoding="utf-8")
|
if lines:
|
||||||
servers = set()
|
if len(lines) > 1:
|
||||||
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()/windows_nameservers()
|
|
||||||
"""
|
|
||||||
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
|
# don't import this unless we really need it
|
||||||
import random
|
import random
|
||||||
random.shuffle(ns_list)
|
random.shuffle(lines)
|
||||||
return ns_list[0]
|
return lines[0]
|
||||||
else:
|
else:
|
||||||
return (socket.AF_INET, '127.0.0.1')
|
return (socket.AF_INET, '127.0.0.1')
|
||||||
|
|
||||||
@ -187,163 +99,3 @@ def family_to_string(family):
|
|||||||
return "AF_INET"
|
return "AF_INET"
|
||||||
else:
|
else:
|
||||||
return str(family)
|
return str(family)
|
||||||
|
|
||||||
|
|
||||||
def get_env():
|
|
||||||
"""An environment for sshuttle subprocesses. See get_path()."""
|
|
||||||
env = {
|
|
||||||
'PATH': get_path(),
|
|
||||||
'LC_ALL': "C",
|
|
||||||
}
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def get_path():
|
|
||||||
"""Returns a string of paths separated by os.pathsep.
|
|
||||||
|
|
||||||
Users might not have all of the programs sshuttle needs in their
|
|
||||||
PATH variable (i.e., some programs might be in /sbin). Use PATH
|
|
||||||
and a hardcoded set of paths to search through. This function is
|
|
||||||
used by our which() and get_env() functions. If which() and the
|
|
||||||
subprocess environments differ, programs that which() finds might
|
|
||||||
not be found at run time (or vice versa).
|
|
||||||
"""
|
|
||||||
path = []
|
|
||||||
if "PATH" in os.environ:
|
|
||||||
path += os.environ["PATH"].split(os.pathsep)
|
|
||||||
# Python default paths.
|
|
||||||
path += os.defpath.split(os.pathsep)
|
|
||||||
# /sbin, etc are not in os.defpath and may not be in PATH either.
|
|
||||||
# /bin/ and /usr/bin below are probably redundant.
|
|
||||||
path += ['/bin', '/usr/bin', '/sbin', '/usr/sbin']
|
|
||||||
|
|
||||||
# Remove duplicates. Not strictly necessary.
|
|
||||||
path_dedup = []
|
|
||||||
for i in path:
|
|
||||||
if i not in path_dedup:
|
|
||||||
path_dedup.append(i)
|
|
||||||
|
|
||||||
return os.pathsep.join(path_dedup)
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 3):
|
|
||||||
from shutil import which as _which
|
|
||||||
else:
|
|
||||||
# Although sshuttle does not officially support older versions of
|
|
||||||
# Python, some still run the sshuttle server on remote machines
|
|
||||||
# with old versions of python.
|
|
||||||
def _which(file, mode=os.F_OK | os.X_OK, path=None):
|
|
||||||
if path is not None:
|
|
||||||
search_paths = path.split(os.pathsep)
|
|
||||||
elif "PATH" in os.environ:
|
|
||||||
search_paths = os.environ["PATH"].split(os.pathsep)
|
|
||||||
else:
|
|
||||||
search_paths = os.defpath.split(os.pathsep)
|
|
||||||
|
|
||||||
for p in search_paths:
|
|
||||||
filepath = os.path.join(p, file)
|
|
||||||
if os.path.exists(filepath) and os.access(filepath, mode):
|
|
||||||
return filepath
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def which(file, mode=os.F_OK | os.X_OK):
|
|
||||||
"""A wrapper around shutil.which() that searches a predictable set of
|
|
||||||
paths and is more verbose about what is happening. See get_path()
|
|
||||||
for more information.
|
|
||||||
"""
|
|
||||||
path = get_path()
|
|
||||||
rv = _which(file, mode, path)
|
|
||||||
if rv:
|
|
||||||
debug2("which() found '%s' at %s" % (file, rv))
|
|
||||||
else:
|
|
||||||
debug2("which() could not find '%s' in %s" % (file, path))
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
@ -9,24 +9,22 @@ import platform
|
|||||||
|
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, get_env
|
from sshuttle.helpers import log, debug1, debug2, debug3
|
||||||
|
|
||||||
POLL_TIME = 60 * 15
|
POLL_TIME = 60 * 15
|
||||||
NETSTAT_POLL_TIME = 30
|
NETSTAT_POLL_TIME = 30
|
||||||
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
|
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
|
||||||
|
|
||||||
# Have we already failed to write CACHEFILE?
|
|
||||||
CACHE_WRITE_FAILED = False
|
|
||||||
|
|
||||||
SHOULD_WRITE_CACHE = False
|
|
||||||
|
|
||||||
|
_nmb_ok = True
|
||||||
|
_smb_ok = True
|
||||||
hostnames = {}
|
hostnames = {}
|
||||||
queue = {}
|
queue = {}
|
||||||
try:
|
try:
|
||||||
null = open(os.devnull, 'wb')
|
null = open(os.devnull, 'wb')
|
||||||
except IOError:
|
except IOError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log('warning: %s' % e)
|
log('warning: %s\n' % e)
|
||||||
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
||||||
|
|
||||||
|
|
||||||
@ -35,10 +33,7 @@ def _is_ip(s):
|
|||||||
|
|
||||||
|
|
||||||
def write_host_cache():
|
def write_host_cache():
|
||||||
"""If possible, write our hosts file to disk so future connections
|
|
||||||
can reuse the hosts that we already found."""
|
|
||||||
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
|
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
|
||||||
global CACHE_WRITE_FAILED
|
|
||||||
try:
|
try:
|
||||||
f = open(tmpname, 'wb')
|
f = open(tmpname, 'wb')
|
||||||
for name, ip in sorted(hostnames.items()):
|
for name, ip in sorted(hostnames.items()):
|
||||||
@ -46,55 +41,33 @@ def write_host_cache():
|
|||||||
f.close()
|
f.close()
|
||||||
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
|
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
|
||||||
os.rename(tmpname, CACHEFILE)
|
os.rename(tmpname, CACHEFILE)
|
||||||
CACHE_WRITE_FAILED = False
|
finally:
|
||||||
except (OSError, IOError):
|
|
||||||
# Write message if we haven't yet or if we get a failure after
|
|
||||||
# a previous success.
|
|
||||||
if not CACHE_WRITE_FAILED:
|
|
||||||
log("Failed to write host cache to temporary file "
|
|
||||||
"%s and rename it to %s" % (tmpname, CACHEFILE))
|
|
||||||
CACHE_WRITE_FAILED = True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.unlink(tmpname)
|
os.unlink(tmpname)
|
||||||
except Exception:
|
except BaseException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def read_host_cache():
|
def read_host_cache():
|
||||||
"""If possible, read the cache file from disk to populate hosts that
|
|
||||||
were found in a previous sshuttle run."""
|
|
||||||
try:
|
try:
|
||||||
f = open(CACHEFILE)
|
f = open(CACHEFILE)
|
||||||
except (OSError, IOError):
|
except IOError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
log("Failed to read existing host cache file %s on remote host"
|
raise
|
||||||
% CACHEFILE)
|
|
||||||
return
|
|
||||||
for line in f:
|
for line in f:
|
||||||
words = line.strip().split(',')
|
words = line.strip().split(',')
|
||||||
if len(words) == 2:
|
if len(words) == 2:
|
||||||
(name, ip) = words
|
(name, ip) = words
|
||||||
name = re.sub(r'[^-\w\.]', '-', name).strip()
|
name = re.sub(r'[^-\w\.]', '-', name).strip()
|
||||||
# Remove characters that shouldn't be in IP
|
|
||||||
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
||||||
if name and ip:
|
if name and ip:
|
||||||
found_host(name, ip)
|
found_host(name, ip)
|
||||||
f.close()
|
|
||||||
global SHOULD_WRITE_CACHE
|
|
||||||
if SHOULD_WRITE_CACHE:
|
|
||||||
write_host_cache()
|
|
||||||
SHOULD_WRITE_CACHE = False
|
|
||||||
|
|
||||||
|
|
||||||
def found_host(name, ip):
|
def found_host(name, ip):
|
||||||
"""The provided name maps to the given IP. Add the host to the
|
|
||||||
hostnames list, send the host to the sshuttle client via
|
|
||||||
stdout, and write the host to the cache file.
|
|
||||||
"""
|
|
||||||
hostname = re.sub(r'\..*', '', name)
|
hostname = re.sub(r'\..*', '', name)
|
||||||
hostname = re.sub(r'[^-\w\.]', '_', hostname)
|
hostname = re.sub(r'[^-\w\.]', '_', hostname)
|
||||||
if (ip.startswith('127.') or ip.startswith('255.') or
|
if (ip.startswith('127.') or ip.startswith('255.') or
|
||||||
@ -104,55 +77,46 @@ def found_host(name, ip):
|
|||||||
if hostname != name:
|
if hostname != name:
|
||||||
found_host(hostname, ip)
|
found_host(hostname, ip)
|
||||||
|
|
||||||
global SHOULD_WRITE_CACHE
|
|
||||||
oldip = hostnames.get(name)
|
oldip = hostnames.get(name)
|
||||||
if oldip != ip:
|
if oldip != ip:
|
||||||
hostnames[name] = ip
|
hostnames[name] = ip
|
||||||
debug1('Found: %s: %s' % (name, ip))
|
debug1('Found: %s: %s\n' % (name, ip))
|
||||||
sys.stdout.write('%s,%s\n' % (name, ip))
|
sys.stdout.write('%s,%s\n' % (name, ip))
|
||||||
SHOULD_WRITE_CACHE = True
|
write_host_cache()
|
||||||
|
|
||||||
|
|
||||||
def _check_etc_hosts():
|
def _check_etc_hosts():
|
||||||
"""If possible, read /etc/hosts to find hosts."""
|
debug2(' > hosts\n')
|
||||||
filename = '/etc/hosts'
|
for line in open('/etc/hosts'):
|
||||||
debug2(' > Reading %s on remote host' % filename)
|
line = re.sub(r'#.*', '', line)
|
||||||
try:
|
words = line.strip().split()
|
||||||
for line in open(filename):
|
if not words:
|
||||||
line = re.sub(r'#.*', '', line) # remove comments
|
continue
|
||||||
words = line.strip().split()
|
ip = words[0]
|
||||||
if not words:
|
names = words[1:]
|
||||||
continue
|
if _is_ip(ip):
|
||||||
ip = words[0]
|
debug3('< %s %r\n' % (ip, names))
|
||||||
if _is_ip(ip):
|
for n in names:
|
||||||
names = words[1:]
|
check_host(n)
|
||||||
debug3('< %s %r' % (ip, names))
|
found_host(n, ip)
|
||||||
for n in names:
|
|
||||||
check_host(n)
|
|
||||||
found_host(n, ip)
|
|
||||||
except (OSError, IOError):
|
|
||||||
debug1("Failed to read %s on remote host" % filename)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_revdns(ip):
|
def _check_revdns(ip):
|
||||||
"""Use reverse DNS to try to get hostnames from an IP addresses."""
|
debug2(' > rev: %s\n' % ip)
|
||||||
debug2(' > rev: %s' % ip)
|
|
||||||
try:
|
try:
|
||||||
r = socket.gethostbyaddr(ip)
|
r = socket.gethostbyaddr(ip)
|
||||||
debug3('< %s' % r[0])
|
debug3('< %s\n' % r[0])
|
||||||
check_host(r[0])
|
check_host(r[0])
|
||||||
found_host(r[0], ip)
|
found_host(r[0], ip)
|
||||||
except (OSError, socket.error, UnicodeError):
|
except (socket.herror, UnicodeError):
|
||||||
# This case is expected to occur regularly.
|
|
||||||
# debug3('< %s gethostbyaddr failed on remote host' % ip)
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _check_dns(hostname):
|
def _check_dns(hostname):
|
||||||
debug2(' > dns: %s' % hostname)
|
debug2(' > dns: %s\n' % hostname)
|
||||||
try:
|
try:
|
||||||
ip = socket.gethostbyname(hostname)
|
ip = socket.gethostbyname(hostname)
|
||||||
debug3('< %s' % ip)
|
debug3('< %s\n' % ip)
|
||||||
check_host(ip)
|
check_host(ip)
|
||||||
found_host(hostname, ip)
|
found_host(hostname, ip)
|
||||||
except (socket.gaierror, UnicodeError):
|
except (socket.gaierror, UnicodeError):
|
||||||
@ -160,35 +124,139 @@ def _check_dns(hostname):
|
|||||||
|
|
||||||
|
|
||||||
def _check_netstat():
|
def _check_netstat():
|
||||||
debug2(' > netstat')
|
debug2(' > netstat\n')
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
argv = ['netstat', '-n']
|
argv = ['netstat', '-n']
|
||||||
try:
|
try:
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||||
env=get_env())
|
env=env)
|
||||||
content = p.stdout.read().decode("ASCII")
|
content = p.stdout.read().decode("ASCII")
|
||||||
p.wait()
|
p.wait()
|
||||||
except OSError:
|
except OSError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
# The same IPs may appear multiple times. Consolidate them so the
|
|
||||||
# debug message doesn't print the same IP repeatedly.
|
|
||||||
ip_list = []
|
|
||||||
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
|
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
|
||||||
if ip not in ip_list:
|
debug3('< %s\n' % ip)
|
||||||
ip_list.append(ip)
|
|
||||||
|
|
||||||
for ip in sorted(ip_list):
|
|
||||||
debug3('< %s' % ip)
|
|
||||||
check_host(ip)
|
check_host(ip)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_smb(hostname):
|
||||||
|
return
|
||||||
|
global _smb_ok
|
||||||
|
if not _smb_ok:
|
||||||
|
return
|
||||||
|
debug2(' > smb: %s\n' % hostname)
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
argv = ['smbclient', '-U', '%', '-L', hostname]
|
||||||
|
try:
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||||
|
env=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))
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
|
||||||
|
try:
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||||
|
env=env)
|
||||||
|
lines = p.stdout.readlines()
|
||||||
|
rv = p.wait()
|
||||||
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
|
log('%r failed: %r\n' % (argv, e))
|
||||||
|
_nmb_ok = False
|
||||||
|
return
|
||||||
|
if rv:
|
||||||
|
log('%r returned %d\n' % (argv, rv))
|
||||||
|
return
|
||||||
|
for line in lines:
|
||||||
|
m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line)
|
||||||
|
if m:
|
||||||
|
g = m.groups()
|
||||||
|
(ip, name) = (g[0], g[1].lower())
|
||||||
|
debug3('< %s -> %s\n' % (name, ip))
|
||||||
|
if is_workgroup:
|
||||||
|
_enqueue(_check_smb, ip)
|
||||||
|
else:
|
||||||
|
found_host(name, ip)
|
||||||
|
check_host(name)
|
||||||
|
|
||||||
|
|
||||||
def check_host(hostname):
|
def check_host(hostname):
|
||||||
if _is_ip(hostname):
|
if _is_ip(hostname):
|
||||||
_enqueue(_check_revdns, hostname)
|
_enqueue(_check_revdns, hostname)
|
||||||
else:
|
else:
|
||||||
_enqueue(_check_dns, hostname)
|
_enqueue(_check_dns, hostname)
|
||||||
|
_enqueue(_check_smb, hostname)
|
||||||
|
_enqueue(_check_nmb, hostname, False, False)
|
||||||
|
|
||||||
|
|
||||||
|
def check_workgroup(hostname):
|
||||||
|
_enqueue(_check_nmb, hostname, True, False)
|
||||||
|
_enqueue(_check_nmb, hostname, True, True)
|
||||||
|
|
||||||
|
|
||||||
def _enqueue(op, *args):
|
def _enqueue(op, *args):
|
||||||
@ -207,9 +275,12 @@ def _stdin_still_ok(timeout):
|
|||||||
|
|
||||||
|
|
||||||
def hw_main(seed_hosts, auto_hosts):
|
def hw_main(seed_hosts, auto_hosts):
|
||||||
helpers.logprefix = 'HH: '
|
if helpers.verbose >= 2:
|
||||||
|
helpers.logprefix = 'HH: '
|
||||||
|
else:
|
||||||
|
helpers.logprefix = 'hostwatch: '
|
||||||
|
|
||||||
debug1('Starting hostwatch with Python version %s'
|
debug1('Starting hostwatch with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
for h in seed_hosts:
|
for h in seed_hosts:
|
||||||
@ -221,22 +292,18 @@ def hw_main(seed_hosts, auto_hosts):
|
|||||||
_enqueue(_check_netstat)
|
_enqueue(_check_netstat)
|
||||||
check_host('localhost')
|
check_host('localhost')
|
||||||
check_host(socket.gethostname())
|
check_host(socket.gethostname())
|
||||||
|
check_workgroup('workgroup')
|
||||||
|
check_workgroup('-')
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
# For each item in the queue
|
|
||||||
for t, last_polled in list(queue.items()):
|
for t, last_polled in list(queue.items()):
|
||||||
(op, args) = t
|
(op, args) = t
|
||||||
if not _stdin_still_ok(0):
|
if not _stdin_still_ok(0):
|
||||||
break
|
break
|
||||||
|
|
||||||
# Determine if we need to run.
|
|
||||||
maxtime = POLL_TIME
|
maxtime = POLL_TIME
|
||||||
# netstat runs more often than other jobs
|
|
||||||
if op == _check_netstat:
|
if op == _check_netstat:
|
||||||
maxtime = NETSTAT_POLL_TIME
|
maxtime = NETSTAT_POLL_TIME
|
||||||
|
|
||||||
# Check if this jobs needs to run.
|
|
||||||
if now - last_polled > maxtime:
|
if now - last_polled > maxtime:
|
||||||
queue[t] = time.time()
|
queue[t] = time.time()
|
||||||
op(*args)
|
op(*args)
|
||||||
@ -246,5 +313,5 @@ def hw_main(seed_hosts, auto_hosts):
|
|||||||
break
|
break
|
||||||
|
|
||||||
# FIXME: use a smarter timeout based on oldest last_polled
|
# FIXME: use a smarter timeout based on oldest last_polled
|
||||||
if not _stdin_still_ok(1): # sleeps for up to 1 second
|
if not _stdin_still_ok(1):
|
||||||
break
|
break
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
from sshuttle.helpers import log, debug1, Fatal, family_to_string, get_env
|
from sshuttle.helpers import log, debug1, Fatal, family_to_string
|
||||||
|
|
||||||
|
|
||||||
def nonfatal(func, *args):
|
def nonfatal(func, *args):
|
||||||
try:
|
try:
|
||||||
func(*args)
|
func(*args)
|
||||||
except Fatal as e:
|
except Fatal as e:
|
||||||
log('error: %s' % e)
|
log('error: %s\n' % e)
|
||||||
|
|
||||||
|
|
||||||
def ipt_chain_exists(family, table, name):
|
def ipt_chain_exists(family, table, name):
|
||||||
@ -17,10 +18,14 @@ def ipt_chain_exists(family, table, name):
|
|||||||
cmd = 'iptables'
|
cmd = 'iptables'
|
||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
argv = [cmd, '-w', '-t', table, '-nL']
|
argv = [cmd, '-t', table, '-nL']
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
output = ssubprocess.check_output(argv, env=get_env())
|
output = ssubprocess.check_output(argv, env=env)
|
||||||
for line in output.decode('ASCII', errors='replace').split('\n'):
|
for line in output.decode('ASCII').split('\n'):
|
||||||
if line.startswith('Chain %s ' % name):
|
if line.startswith('Chain %s ' % name):
|
||||||
return True
|
return True
|
||||||
except ssubprocess.CalledProcessError as e:
|
except ssubprocess.CalledProcessError as e:
|
||||||
@ -29,13 +34,17 @@ def ipt_chain_exists(family, table, name):
|
|||||||
|
|
||||||
def ipt(family, table, *args):
|
def ipt(family, table, *args):
|
||||||
if family == socket.AF_INET6:
|
if family == socket.AF_INET6:
|
||||||
argv = ['ip6tables', '-w', '-t', table] + list(args)
|
argv = ['ip6tables', '-t', table] + list(args)
|
||||||
elif family == socket.AF_INET:
|
elif family == socket.AF_INET:
|
||||||
argv = ['iptables', '-w', '-t', table] + list(args)
|
argv = ['iptables', '-t', table] + list(args)
|
||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
debug1('%s' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv, env=get_env())
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
rv = ssubprocess.call(argv, env=env)
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
@ -45,7 +54,33 @@ def nft(family, table, action, *args):
|
|||||||
argv = ['nft', action, 'inet', table] + list(args)
|
argv = ['nft', action, 'inet', table] + list(args)
|
||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
debug1('%s' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv, env=get_env())
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
rv = ssubprocess.call(argv, env=env)
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
_no_ttl_module = False
|
||||||
|
|
||||||
|
|
||||||
|
def ipt_ttl(family, *args):
|
||||||
|
global _no_ttl_module
|
||||||
|
if not _no_ttl_module:
|
||||||
|
# we avoid infinite loops by generating server-side connections
|
||||||
|
# with ttl 42. This makes the client side not recapture those
|
||||||
|
# connections, in case client == server.
|
||||||
|
try:
|
||||||
|
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
|
||||||
|
ipt(family, *argsplus)
|
||||||
|
except Fatal:
|
||||||
|
ipt(family, *args)
|
||||||
|
# we only get here if the non-ttl attempt succeeds
|
||||||
|
log('sshuttle: warning: your iptables is missing '
|
||||||
|
'the ttl module.\n')
|
||||||
|
_no_ttl_module = True
|
||||||
|
else:
|
||||||
|
ipt(family, *args)
|
||||||
|
@ -1,34 +1,26 @@
|
|||||||
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import sys
|
|
||||||
import errno
|
import errno
|
||||||
import ipaddress
|
|
||||||
from sshuttle.helpers import Fatal, debug3
|
from sshuttle.helpers import Fatal, debug3
|
||||||
|
|
||||||
|
|
||||||
def original_dst(sock):
|
def original_dst(sock):
|
||||||
try:
|
try:
|
||||||
family = sock.family
|
|
||||||
SO_ORIGINAL_DST = 80
|
SO_ORIGINAL_DST = 80
|
||||||
|
SOCKADDR_MIN = 16
|
||||||
if family == socket.AF_INET:
|
sockaddr_in = sock.getsockopt(socket.SOL_IP,
|
||||||
SOCKADDR_MIN = 16
|
SO_ORIGINAL_DST, SOCKADDR_MIN)
|
||||||
sockaddr_in = sock.getsockopt(socket.SOL_IP,
|
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
|
||||||
SO_ORIGINAL_DST, SOCKADDR_MIN)
|
# FIXME: decoding is IPv4 only.
|
||||||
port, raw_ip = struct.unpack_from('!2xH4s', sockaddr_in[:8])
|
assert(socket.htons(proto) == socket.AF_INET)
|
||||||
ip = str(ipaddress.IPv4Address(raw_ip))
|
ip = '%d.%d.%d.%d' % (a, b, c, d)
|
||||||
elif family == socket.AF_INET6:
|
return (ip, port)
|
||||||
sockaddr_in = sock.getsockopt(41, SO_ORIGINAL_DST, 64)
|
|
||||||
port, raw_ip = struct.unpack_from("!2xH4x16s", sockaddr_in)
|
|
||||||
ip = str(ipaddress.IPv6Address(raw_ip))
|
|
||||||
else:
|
|
||||||
raise Fatal("fw: Unknown family type.")
|
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.ENOPROTOOPT:
|
if e.args[0] == errno.ENOPROTOOPT:
|
||||||
return sock.getsockname()
|
return sock.getsockname()
|
||||||
raise
|
raise
|
||||||
return (ip, port)
|
|
||||||
|
|
||||||
|
|
||||||
class Features(object):
|
class Features(object):
|
||||||
@ -46,35 +38,26 @@ class BaseMethod(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_supported_features():
|
def get_supported_features():
|
||||||
result = Features()
|
result = Features()
|
||||||
result.loopback_proxy_port = True
|
|
||||||
result.ipv4 = True
|
|
||||||
result.ipv6 = False
|
result.ipv6 = False
|
||||||
result.udp = False
|
result.udp = False
|
||||||
result.dns = True
|
result.dns = True
|
||||||
result.user = False
|
result.user = False
|
||||||
result.group = False
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_supported():
|
|
||||||
"""Returns true if it appears that this method will work on this
|
|
||||||
machine."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_tcp_dstip(sock):
|
def get_tcp_dstip(sock):
|
||||||
return original_dst(sock)
|
return original_dst(sock)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def recv_udp(udp_listener, bufsize):
|
def recv_udp(udp_listener, bufsize):
|
||||||
debug3('Accept UDP using recvfrom.')
|
debug3('Accept UDP using recvfrom.\n')
|
||||||
data, srcip = udp_listener.recvfrom(bufsize)
|
data, srcip = udp_listener.recvfrom(bufsize)
|
||||||
return (srcip, None, data)
|
return (srcip, None, data)
|
||||||
|
|
||||||
def send_udp(self, sock, srcip, dstip, data):
|
def send_udp(self, sock, srcip, dstip, data):
|
||||||
if srcip is not None:
|
if srcip is not None:
|
||||||
raise Fatal("Method %s send_udp does not support setting srcip to %r"
|
Fatal("Method %s send_udp does not support setting srcip to %r"
|
||||||
% (self.name, srcip))
|
% (self.name, srcip))
|
||||||
sock.sendto(data, dstip)
|
sock.sendto(data, dstip)
|
||||||
|
|
||||||
def setup_tcp_listener(self, tcp_listener):
|
def setup_tcp_listener(self, tcp_listener):
|
||||||
@ -85,20 +68,17 @@ class BaseMethod(object):
|
|||||||
|
|
||||||
def assert_features(self, features):
|
def assert_features(self, features):
|
||||||
avail = self.get_supported_features()
|
avail = self.get_supported_features()
|
||||||
for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
|
for key in ["udp", "dns", "ipv6", "user"]:
|
||||||
if getattr(features, key) and not getattr(avail, key):
|
if getattr(features, key) and not getattr(avail, key):
|
||||||
raise Fatal(
|
raise Fatal(
|
||||||
"Feature %s not supported with method %s." %
|
"Feature %s not supported with method %s.\n" %
|
||||||
(key, self.name))
|
(key, self.name))
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user, group, tmark):
|
user):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user, group):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def wait_for_firewall_ready(self, sshuttle_pid):
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -106,21 +86,30 @@ class BaseMethod(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _program_exists(name):
|
||||||
|
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
||||||
|
for p in paths:
|
||||||
|
fn = '%s/%s' % (p, name)
|
||||||
|
if os.path.exists(fn):
|
||||||
|
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
||||||
|
|
||||||
|
|
||||||
def get_method(method_name):
|
def get_method(method_name):
|
||||||
module = importlib.import_module("sshuttle.methods.%s" % method_name)
|
module = importlib.import_module("sshuttle.methods.%s" % method_name)
|
||||||
return module.Method(method_name)
|
return module.Method(method_name)
|
||||||
|
|
||||||
|
|
||||||
def get_auto_method():
|
def get_auto_method():
|
||||||
debug3("Selecting a method automatically...")
|
if _program_exists('iptables'):
|
||||||
# Try these methods, in order:
|
method_name = "nat"
|
||||||
methods_to_try = ["nat", "nft", "pf", "ipfw"] if sys.platform != "win32" else ["windivert"]
|
elif _program_exists('nft'):
|
||||||
for m in methods_to_try:
|
method_name = "nft"
|
||||||
method = get_method(m)
|
elif _program_exists('pfctl'):
|
||||||
if method.is_supported():
|
method_name = "pf"
|
||||||
debug3("Method '%s' was automatically selected." % m)
|
elif _program_exists('ipfw'):
|
||||||
return method
|
method_name = "ipfw"
|
||||||
|
else:
|
||||||
|
raise Fatal(
|
||||||
|
"can't find either iptables, nft or pfctl; check your PATH")
|
||||||
|
|
||||||
raise Fatal("Unable to automatically find a supported method. Check that "
|
return get_method(method_name)
|
||||||
"the appropriate programs are in your PATH. We tried "
|
|
||||||
"methods: %s" % str(methods_to_try))
|
|
||||||
|
@ -1,43 +1,79 @@
|
|||||||
import os
|
import os
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, \
|
from sshuttle.helpers import log, debug1, debug3, \
|
||||||
Fatal, family_to_string, get_env, which
|
Fatal, family_to_string
|
||||||
|
|
||||||
import socket
|
recvmsg = None
|
||||||
|
try:
|
||||||
|
# try getting recvmsg from python
|
||||||
|
import socket as pythonsocket
|
||||||
|
getattr(pythonsocket.socket, "recvmsg")
|
||||||
|
socket = pythonsocket
|
||||||
|
recvmsg = "python"
|
||||||
|
except AttributeError:
|
||||||
|
# try getting recvmsg from socket_ext library
|
||||||
|
try:
|
||||||
|
import socket_ext
|
||||||
|
getattr(socket_ext.socket, "recvmsg")
|
||||||
|
socket = socket_ext
|
||||||
|
recvmsg = "socket_ext"
|
||||||
|
except ImportError:
|
||||||
|
import socket
|
||||||
|
|
||||||
IP_BINDANY = 24
|
IP_BINDANY = 24
|
||||||
IP_RECVDSTADDR = 7
|
IP_RECVDSTADDR = 7
|
||||||
SOL_IPV6 = 41
|
SOL_IPV6 = 41
|
||||||
IPV6_RECVDSTADDR = 74
|
IPV6_RECVDSTADDR = 74
|
||||||
|
|
||||||
|
if recvmsg == "python":
|
||||||
def recv_udp(listener, bufsize):
|
def recv_udp(listener, bufsize):
|
||||||
debug3('Accept UDP python using recvmsg.')
|
debug3('Accept UDP python using recvmsg.\n')
|
||||||
data, ancdata, _, srcip = listener.recvmsg(4096,
|
data, ancdata, _, srcip = listener.recvmsg(4096,
|
||||||
socket.CMSG_SPACE(4))
|
socket.CMSG_SPACE(4))
|
||||||
dstip = None
|
dstip = None
|
||||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
||||||
port = 53
|
port = 53
|
||||||
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
|
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
|
||||||
dstip = (ip, port)
|
dstip = (ip, port)
|
||||||
break
|
break
|
||||||
return (srcip, dstip, data)
|
return (srcip, dstip, data)
|
||||||
|
elif recvmsg == "socket_ext":
|
||||||
|
def recv_udp(listener, bufsize):
|
||||||
|
debug3('Accept UDP using socket_ext recvmsg.\n')
|
||||||
|
srcip, data, adata, _ = listener.recvmsg((bufsize,),
|
||||||
|
socket.CMSG_SPACE(4))
|
||||||
|
dstip = None
|
||||||
|
for a in adata:
|
||||||
|
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR:
|
||||||
|
port = 53
|
||||||
|
ip = socket.inet_ntop(socket.AF_INET, a.cmsg_data[0:4])
|
||||||
|
dstip = (ip, port)
|
||||||
|
break
|
||||||
|
return (srcip, dstip, data[0])
|
||||||
|
else:
|
||||||
|
def recv_udp(listener, bufsize):
|
||||||
|
debug3('Accept UDP using recvfrom.\n')
|
||||||
|
data, srcip = listener.recvfrom(bufsize)
|
||||||
|
return (srcip, None, data)
|
||||||
|
|
||||||
|
|
||||||
def ipfw_rule_exists(n):
|
def ipfw_rule_exists(n):
|
||||||
argv = ['ipfw', 'list', '%d' % n]
|
argv = ['ipfw', 'list']
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||||
|
|
||||||
found = False
|
found = False
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
if line.startswith(b'%05d ' % n):
|
if line.startswith(b'%05d ' % n):
|
||||||
if 'check-state :sshuttle' not in line:
|
if not ('ipttl 42' in line or 'check-state' in line):
|
||||||
log('non-sshuttle ipfw rule: %r' % line.strip())
|
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
||||||
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||||
found = True
|
found = True
|
||||||
break
|
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
@ -49,10 +85,14 @@ _oldctls = {}
|
|||||||
|
|
||||||
def _fill_oldctls(prefix):
|
def _fill_oldctls(prefix):
|
||||||
argv = ['sysctl', prefix]
|
argv = ['sysctl', prefix]
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
line = line.decode()
|
line = line.decode()
|
||||||
assert line[-1] == '\n'
|
assert(line[-1] == '\n')
|
||||||
(k, v) = line[:-1].split(': ', 1)
|
(k, v) = line[:-1].split(': ', 1)
|
||||||
_oldctls[k] = v.strip()
|
_oldctls[k] = v.strip()
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
@ -64,8 +104,8 @@ def _fill_oldctls(prefix):
|
|||||||
|
|
||||||
def _sysctl_set(name, val):
|
def _sysctl_set(name, val):
|
||||||
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||||
debug1('>> %s' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
|
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'))
|
||||||
# No env: No output. (Or error that won't be parsed.)
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
|
|
||||||
|
|
||||||
@ -74,18 +114,18 @@ _changedctls = []
|
|||||||
|
|
||||||
def sysctl_set(name, val, permanent=False):
|
def sysctl_set(name, val, permanent=False):
|
||||||
PREFIX = 'net.inet.ip'
|
PREFIX = 'net.inet.ip'
|
||||||
assert name.startswith(PREFIX + '.')
|
assert(name.startswith(PREFIX + '.'))
|
||||||
val = str(val)
|
val = str(val)
|
||||||
if not _oldctls:
|
if not _oldctls:
|
||||||
_fill_oldctls(PREFIX)
|
_fill_oldctls(PREFIX)
|
||||||
if not (name in _oldctls):
|
if not (name in _oldctls):
|
||||||
debug1('>> No such sysctl: %r' % name)
|
debug1('>> No such sysctl: %r\n' % name)
|
||||||
return False
|
return False
|
||||||
oldval = _oldctls[name]
|
oldval = _oldctls[name]
|
||||||
if val != oldval:
|
if val != oldval:
|
||||||
rv = _sysctl_set(name, val)
|
rv = _sysctl_set(name, val)
|
||||||
if rv == 0 and permanent:
|
if rv == 0 and permanent:
|
||||||
debug1('>> ...saving permanently in /etc/sysctl.conf')
|
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
|
||||||
f = open('/etc/sysctl.conf', 'a')
|
f = open('/etc/sysctl.conf', 'a')
|
||||||
f.write('\n'
|
f.write('\n'
|
||||||
'# Added by sshuttle\n'
|
'# Added by sshuttle\n'
|
||||||
@ -98,8 +138,8 @@ def sysctl_set(name, val, permanent=False):
|
|||||||
|
|
||||||
def ipfw(*args):
|
def ipfw(*args):
|
||||||
argv = ['ipfw', '-q'] + list(args)
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
debug1('>> %s' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv, env=get_env())
|
rv = ssubprocess.call(argv)
|
||||||
# No env: No output. (Or error that won't be parsed.)
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
@ -107,8 +147,8 @@ def ipfw(*args):
|
|||||||
|
|
||||||
def ipfw_noexit(*args):
|
def ipfw_noexit(*args):
|
||||||
argv = ['ipfw', '-q'] + list(args)
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
debug1('>> %s' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
ssubprocess.call(argv, env=get_env())
|
ssubprocess.call(argv)
|
||||||
# No env: No output. (Or error that won't be parsed.)
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
|
|
||||||
|
|
||||||
@ -129,7 +169,7 @@ class Method(BaseMethod):
|
|||||||
if not dstip:
|
if not dstip:
|
||||||
debug1(
|
debug1(
|
||||||
"-- ignored UDP from %r: "
|
"-- ignored UDP from %r: "
|
||||||
"couldn't determine destination IP address" % (srcip,))
|
"couldn't determine destination IP address\n" % (srcip,))
|
||||||
return None
|
return None
|
||||||
return srcip, dstip, data
|
return srcip, dstip, data
|
||||||
|
|
||||||
@ -137,14 +177,15 @@ class Method(BaseMethod):
|
|||||||
if not srcip:
|
if not srcip:
|
||||||
debug1(
|
debug1(
|
||||||
"-- ignored UDP to %r: "
|
"-- ignored UDP to %r: "
|
||||||
"couldn't determine source IP address" % (dstip,))
|
"couldn't determine source IP address\n" % (dstip,))
|
||||||
return
|
return
|
||||||
|
|
||||||
# debug3('Sending SRC: %r DST: %r' % (srcip, dstip))
|
# debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
|
||||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||||
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||||
|
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
sender.bind(srcip)
|
sender.bind(srcip)
|
||||||
sender.sendto(data, dstip)
|
sender.sendto(data, dstip)
|
||||||
sender.close()
|
sender.close()
|
||||||
@ -156,7 +197,7 @@ class Method(BaseMethod):
|
|||||||
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user, group, tmark):
|
user):
|
||||||
# IPv6 not supported
|
# IPv6 not supported
|
||||||
if family not in [socket.AF_INET]:
|
if family not in [socket.AF_INET]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
@ -174,7 +215,8 @@ class Method(BaseMethod):
|
|||||||
if subnets or dnsport:
|
if subnets or dnsport:
|
||||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||||
|
|
||||||
ipfw('add', '1', 'check-state', ':sshuttle')
|
ipfw('add', '1', 'check-state', 'ip',
|
||||||
|
'from', 'any', 'to', 'any')
|
||||||
|
|
||||||
ipfw('add', '1', 'skipto', '2',
|
ipfw('add', '1', 'skipto', '2',
|
||||||
'tcp',
|
'tcp',
|
||||||
@ -182,7 +224,7 @@ class Method(BaseMethod):
|
|||||||
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
|
||||||
'tcp',
|
'tcp',
|
||||||
'from', 'any', 'to', 'table(126)',
|
'from', 'any', 'to', 'table(126)',
|
||||||
'setup', 'keep-state', ':sshuttle')
|
'not', 'ipttl', '42', 'keep-state', 'setup')
|
||||||
|
|
||||||
ipfw_noexit('table', '124', 'flush')
|
ipfw_noexit('table', '124', 'flush')
|
||||||
dnscount = 0
|
dnscount = 0
|
||||||
@ -193,34 +235,29 @@ class Method(BaseMethod):
|
|||||||
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
|
||||||
'udp',
|
'udp',
|
||||||
'from', 'any', 'to', 'table(124)',
|
'from', 'any', 'to', 'table(124)',
|
||||||
'keep-state', ':sshuttle')
|
'not', 'ipttl', '42')
|
||||||
ipfw('add', '1', 'allow',
|
ipfw('add', '1', 'allow',
|
||||||
'udp',
|
'udp',
|
||||||
'from', 'any', 'to', 'any')
|
'from', 'any', 'to', 'any',
|
||||||
|
'ipttl', '42')
|
||||||
|
|
||||||
if subnets:
|
if subnets:
|
||||||
# create new subnet entries
|
# create new subnet entries
|
||||||
for _, swidth, sexclude, snet, fport, lport \
|
for _, swidth, sexclude, snet in sorted(subnets,
|
||||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
key=lambda s: s[1],
|
||||||
|
reverse=True):
|
||||||
if sexclude:
|
if sexclude:
|
||||||
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
||||||
else:
|
else:
|
||||||
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
|
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user, group):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
if family not in [socket.AF_INET]:
|
if family not in [socket.AF_INET]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by ipfw method'
|
'Address family "%s" unsupported by tproxy method'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
|
|
||||||
ipfw_noexit('delete', '1')
|
ipfw_noexit('delete', '1')
|
||||||
ipfw_noexit('table', '124', 'flush')
|
ipfw_noexit('table', '124', 'flush')
|
||||||
ipfw_noexit('table', '125', 'flush')
|
ipfw_noexit('table', '125', 'flush')
|
||||||
ipfw_noexit('table', '126', 'flush')
|
ipfw_noexit('table', '126', 'flush')
|
||||||
|
|
||||||
def is_supported(self):
|
|
||||||
if which("ipfw"):
|
|
||||||
return True
|
|
||||||
debug2("ipfw method not supported because 'ipfw' command is "
|
|
||||||
"missing.")
|
|
||||||
return False
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import socket
|
import socket
|
||||||
from sshuttle.firewall import subnet_weight
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import family_to_string, which, debug2
|
from sshuttle.helpers import family_to_string
|
||||||
from sshuttle.linux import ipt, ipt_chain_exists, nonfatal
|
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
|
||||||
from sshuttle.methods import BaseMethod
|
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
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
# "-A OUTPUT").
|
# "-A OUTPUT").
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user, group, tmark):
|
user):
|
||||||
if family != socket.AF_INET and family != socket.AF_INET6:
|
# only ipv4 supported with NAT
|
||||||
|
if family != socket.AF_INET:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by nat method_name'
|
'Address family "%s" unsupported by nat method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by nat method_name")
|
raise Exception("UDP not supported by nat method_name")
|
||||||
|
|
||||||
table = "nat"
|
table = "nat"
|
||||||
|
|
||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
|
def _ipt_ttl(*args):
|
||||||
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
def _ipm(*args):
|
def _ipm(*args):
|
||||||
return ipt(family, "mangle", *args)
|
return ipt(family, "mangle", *args)
|
||||||
|
|
||||||
chain = 'sshuttle-%s' % port
|
chain = 'sshuttle-%s' % port
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
self.restore_firewall(port, family, udp, user, group)
|
self.restore_firewall(port, family, udp, user)
|
||||||
|
|
||||||
_ipt('-N', chain)
|
_ipt('-N', chain)
|
||||||
_ipt('-F', chain)
|
_ipt('-F', chain)
|
||||||
if user is not None or group is not None:
|
if user is not None:
|
||||||
margs = ['-I', 'OUTPUT', '1', '-m', 'owner']
|
_ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user),
|
||||||
if user is not None:
|
'-j', 'MARK', '--set-mark', str(port))
|
||||||
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
|
args = '-m', 'mark', '--mark', str(port), '-j', chain
|
||||||
else:
|
else:
|
||||||
args = '-j', chain
|
args = '-j', chain
|
||||||
@ -50,14 +50,17 @@ class Method(BaseMethod):
|
|||||||
_ipt('-I', 'OUTPUT', '1', *args)
|
_ipt('-I', 'OUTPUT', '1', *args)
|
||||||
_ipt('-I', 'PREROUTING', '1', *args)
|
_ipt('-I', 'PREROUTING', '1', *args)
|
||||||
|
|
||||||
# Redirect DNS traffic as requested. This includes routing traffic
|
# Firstly we always skip all LOCAL addtrype address, i.e. avoid
|
||||||
# to localhost DNS servers through sshuttle.
|
# tunnelling the traffic designated to all local TCP/IP addresses.
|
||||||
for _, ip in [i for i in nslist if i[0] == family]:
|
_ipt('-A', chain, '-j', 'RETURN',
|
||||||
_ipt('-A', chain, '-j', 'REDIRECT',
|
'-m', 'addrtype',
|
||||||
'--dest', '%s' % ip,
|
'--dst-type', 'LOCAL',
|
||||||
'-p', 'udp',
|
'!', '-p', 'udp')
|
||||||
'--dport', '53',
|
# Skip LOCAL traffic if it's not DNS.
|
||||||
'--to-ports', str(dnsport))
|
_ipt('-A', chain, '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype',
|
||||||
|
'--dst-type', 'LOCAL',
|
||||||
|
'-p', 'udp', '!', '--dport', '53')
|
||||||
|
|
||||||
# create new subnet entries.
|
# create new subnet entries.
|
||||||
for _, swidth, sexclude, snet, fport, lport \
|
for _, swidth, sexclude, snet, fport, lport \
|
||||||
@ -71,18 +74,20 @@ class Method(BaseMethod):
|
|||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
*tcp_ports)
|
*tcp_ports)
|
||||||
else:
|
else:
|
||||||
_ipt('-A', chain, '-j', 'REDIRECT',
|
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
*(tcp_ports + ('--to-ports', str(port))))
|
*(tcp_ports + ('--to-ports', str(port))))
|
||||||
|
|
||||||
# Don't route any remaining local traffic through sshuttle.
|
for _, ip in [i for i in nslist if i[0] == family]:
|
||||||
_ipt('-A', chain, '-j', 'RETURN',
|
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
'-m', 'addrtype',
|
'--dest', '%s/32' % ip,
|
||||||
'--dst-type', 'LOCAL')
|
'-p', 'udp',
|
||||||
|
'--dport', '53',
|
||||||
|
'--to-ports', str(dnsport))
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user, group):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
# only ipv4 supported with NAT
|
# only ipv4 supported with NAT
|
||||||
if family != socket.AF_INET and family != socket.AF_INET6:
|
if family != socket.AF_INET:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by nat method_name'
|
'Address family "%s" unsupported by nat method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
@ -94,6 +99,9 @@ class Method(BaseMethod):
|
|||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
|
def _ipt_ttl(*args):
|
||||||
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
def _ipm(*args):
|
def _ipm(*args):
|
||||||
return ipt(family, "mangle", *args)
|
return ipt(family, "mangle", *args)
|
||||||
|
|
||||||
@ -101,15 +109,9 @@ class Method(BaseMethod):
|
|||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
if ipt_chain_exists(family, table, chain):
|
if ipt_chain_exists(family, table, chain):
|
||||||
if user is not None or group is not None:
|
if user is not None:
|
||||||
margs = ['-D', 'OUTPUT', '-m', 'owner']
|
nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner',
|
||||||
if user is not None:
|
str(user), '-j', 'MARK', '--set-mark', str(port))
|
||||||
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
|
args = '-m', 'mark', '--mark', str(port), '-j', chain
|
||||||
else:
|
else:
|
||||||
args = '-j', chain
|
args = '-j', chain
|
||||||
@ -121,13 +123,4 @@ class Method(BaseMethod):
|
|||||||
def get_supported_features(self):
|
def get_supported_features(self):
|
||||||
result = super(Method, self).get_supported_features()
|
result = super(Method, self).get_supported_features()
|
||||||
result.user = True
|
result.user = True
|
||||||
result.ipv6 = True
|
|
||||||
result.group = True
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def is_supported(self):
|
|
||||||
if which("iptables"):
|
|
||||||
return True
|
|
||||||
debug2("nat method not supported because 'iptables' command "
|
|
||||||
"is missing.")
|
|
||||||
return False
|
|
||||||
|
@ -2,7 +2,6 @@ import socket
|
|||||||
from sshuttle.firewall import subnet_weight
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.linux import nft, nonfatal
|
from sshuttle.linux import nft, nonfatal
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
from sshuttle.helpers import debug2, which
|
|
||||||
|
|
||||||
|
|
||||||
class Method(BaseMethod):
|
class Method(BaseMethod):
|
||||||
@ -13,14 +12,11 @@ class Method(BaseMethod):
|
|||||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
# "-A OUTPUT").
|
# "-A OUTPUT").
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user, group, tmark):
|
user):
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by nft")
|
raise Exception("UDP not supported by nft")
|
||||||
|
|
||||||
if family == socket.AF_INET:
|
table = 'sshuttle-%s' % port
|
||||||
table = 'sshuttle-ipv4-%s' % port
|
|
||||||
if family == socket.AF_INET6:
|
|
||||||
table = 'sshuttle-ipv6-%s' % port
|
|
||||||
|
|
||||||
def _nft(action, *args):
|
def _nft(action, *args):
|
||||||
return nft(family, table, action, *args)
|
return nft(family, table, action, *args)
|
||||||
@ -38,77 +34,43 @@ class Method(BaseMethod):
|
|||||||
_nft('add rule', 'output jump %s' % chain)
|
_nft('add rule', 'output jump %s' % chain)
|
||||||
_nft('add rule', 'prerouting jump %s' % chain)
|
_nft('add rule', 'prerouting jump %s' % chain)
|
||||||
|
|
||||||
# setup_firewall() gets called separately for ipv4 and ipv6. Make sure
|
|
||||||
# we only handle the version that we expect to.
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv4', 'return')
|
|
||||||
else:
|
|
||||||
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return')
|
|
||||||
|
|
||||||
# Strings to use below to simplify our code
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
ip_version_l = 'ipv4'
|
|
||||||
ip_version = 'ip'
|
|
||||||
elif family == socket.AF_INET6:
|
|
||||||
ip_version_l = 'ipv6'
|
|
||||||
ip_version = 'ip6'
|
|
||||||
|
|
||||||
# Redirect DNS traffic as requested. This includes routing traffic
|
|
||||||
# to localhost DNS servers through sshuttle.
|
|
||||||
for _, ip in [i for i in nslist if i[0] == family]:
|
|
||||||
_nft('add rule', chain, ip_version,
|
|
||||||
'daddr %s' % ip, 'udp dport 53',
|
|
||||||
('redirect to :' + str(dnsport)))
|
|
||||||
|
|
||||||
# Don't route any remaining local traffic through sshuttle
|
|
||||||
_nft('add rule', chain, 'fib daddr type local return')
|
|
||||||
|
|
||||||
# create new subnet entries.
|
# create new subnet entries.
|
||||||
for _, swidth, sexclude, snet, fport, lport \
|
for _, swidth, sexclude, snet, fport, lport \
|
||||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||||
|
tcp_ports = ('ip', 'protocol', 'tcp')
|
||||||
# match using nfproto as described at
|
|
||||||
# https://superuser.com/questions/1560376/match-ipv6-protocol-using-nftables
|
|
||||||
if fport and fport != lport:
|
if fport and fport != lport:
|
||||||
tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp',
|
tcp_ports = \
|
||||||
'dport', '{ %d-%d }' % (fport, lport))
|
tcp_ports + \
|
||||||
|
('tcp', 'dport', '{ %d-%d }' % (fport, lport))
|
||||||
elif fport and fport == lport:
|
elif fport and fport == lport:
|
||||||
tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp',
|
tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport))
|
||||||
'dport', '%d' % (fport))
|
|
||||||
else:
|
|
||||||
tcp_ports = ('meta', 'nfproto', ip_version_l,
|
|
||||||
'meta', 'l4proto', 'tcp')
|
|
||||||
|
|
||||||
if sexclude:
|
if sexclude:
|
||||||
_nft('add rule', chain, *(tcp_ports + (
|
_nft('add rule', chain, *(tcp_ports + (
|
||||||
ip_version, 'daddr %s/%s' % (snet, swidth), 'return')))
|
'ip daddr %s/%s' % (snet, swidth), 'return')))
|
||||||
else:
|
else:
|
||||||
_nft('add rule', chain, *(tcp_ports + (
|
_nft('add rule', chain, *(tcp_ports + (
|
||||||
ip_version, 'daddr %s/%s' % (snet, swidth),
|
'ip daddr %s/%s' % (snet, swidth), 'ip ttl != 42',
|
||||||
('redirect to :' + str(port)))))
|
('redirect to :' + str(port)))))
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user, group):
|
for _, ip in [i for i in nslist if i[0] == family]:
|
||||||
|
if family == socket.AF_INET:
|
||||||
|
_nft('add rule', chain, 'ip protocol udp ip daddr %s' % ip,
|
||||||
|
'udp dport { 53 }', 'ip ttl != 42',
|
||||||
|
('redirect to :' + str(dnsport)))
|
||||||
|
elif family == socket.AF_INET6:
|
||||||
|
_nft('add rule', chain, 'ip6 protocol udp ip6 daddr %s' % ip,
|
||||||
|
'udp dport { 53 }', 'ip ttl != 42',
|
||||||
|
('redirect to :' + str(dnsport)))
|
||||||
|
|
||||||
|
def restore_firewall(self, port, family, udp, user):
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by nft method_name")
|
raise Exception("UDP not supported by nft method_name")
|
||||||
|
|
||||||
if family == socket.AF_INET:
|
table = 'sshuttle-%s' % port
|
||||||
table = 'sshuttle-ipv4-%s' % port
|
|
||||||
if family == socket.AF_INET6:
|
|
||||||
table = 'sshuttle-ipv6-%s' % port
|
|
||||||
|
|
||||||
def _nft(action, *args):
|
def _nft(action, *args):
|
||||||
return nft(family, table, action, *args)
|
return nft(family, table, action, *args)
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
nonfatal(_nft, 'delete table', '')
|
nonfatal(_nft, 'delete table', '')
|
||||||
|
|
||||||
def get_supported_features(self):
|
|
||||||
result = super(Method, self).get_supported_features()
|
|
||||||
result.ipv6 = True
|
|
||||||
return result
|
|
||||||
|
|
||||||
def is_supported(self):
|
|
||||||
if which("nft"):
|
|
||||||
return True
|
|
||||||
debug2("nft method not supported because 'nft' command is missing.")
|
|
||||||
return False
|
|
||||||
|
@ -11,8 +11,7 @@ from fcntl import ioctl
|
|||||||
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
||||||
sizeof, addressof, memmove
|
sizeof, addressof, memmove
|
||||||
from sshuttle.firewall import subnet_weight
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, \
|
from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string
|
||||||
family_to_string, get_env, which
|
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
@ -180,7 +179,7 @@ class FreeBsd(Generic):
|
|||||||
return freebsd
|
return freebsd
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
returncode = ssubprocess.call(['kldload', 'pf'], env=get_env())
|
returncode = ssubprocess.call(['kldload', 'pf'])
|
||||||
# No env: No output.
|
# No env: No output.
|
||||||
super(FreeBsd, self).enable()
|
super(FreeBsd, self).enable()
|
||||||
if returncode == 0:
|
if returncode == 0:
|
||||||
@ -190,7 +189,7 @@ class FreeBsd(Generic):
|
|||||||
super(FreeBsd, self).disable(anchor)
|
super(FreeBsd, self).disable(anchor)
|
||||||
if _pf_context['loaded_by_sshuttle'] and \
|
if _pf_context['loaded_by_sshuttle'] and \
|
||||||
_pf_context['started_by_sshuttle'] == 0:
|
_pf_context['started_by_sshuttle'] == 0:
|
||||||
ssubprocess.call(['kldunload', 'pf'], env=get_env())
|
ssubprocess.call(['kldunload', 'pf'])
|
||||||
# No env: No output.
|
# No env: No output.
|
||||||
|
|
||||||
def add_anchors(self, anchor):
|
def add_anchors(self, anchor):
|
||||||
@ -266,14 +265,14 @@ class OpenBsd(Generic):
|
|||||||
("proto_variant", c_uint8),
|
("proto_variant", c_uint8),
|
||||||
("direction", c_uint8)]
|
("direction", c_uint8)]
|
||||||
|
|
||||||
self.pfioc_rule = c_char * 3408
|
self.pfioc_rule = c_char * 3424
|
||||||
self.pfioc_natlook = pfioc_natlook
|
self.pfioc_natlook = pfioc_natlook
|
||||||
super(OpenBsd, self).__init__()
|
super(OpenBsd, self).__init__()
|
||||||
|
|
||||||
def add_anchors(self, anchor):
|
def add_anchors(self, anchor):
|
||||||
# before adding anchors and rules we must override the skip lo
|
# before adding anchors and rules we must override the skip lo
|
||||||
# that comes by default in openbsd pf.conf so the rules we will add,
|
# that comes by default in openbsd pf.conf so the rules we will add,
|
||||||
# which rely on translating/filtering packets on lo, can work
|
# which rely on translating/filtering packets on lo, can work
|
||||||
if self.has_skip_loopback():
|
if self.has_skip_loopback():
|
||||||
pfctl('-f /dev/stdin', b'match on lo\n')
|
pfctl('-f /dev/stdin', b'match on lo\n')
|
||||||
super(OpenBsd, self).add_anchors(anchor)
|
super(OpenBsd, self).add_anchors(anchor)
|
||||||
@ -353,7 +352,7 @@ class Darwin(FreeBsd):
|
|||||||
def add_anchors(self, anchor):
|
def add_anchors(self, anchor):
|
||||||
# before adding anchors and rules we must override the skip lo
|
# before adding anchors and rules we must override the skip lo
|
||||||
# that in some cases ends up in the chain so the rules we will add,
|
# that in some cases ends up in the chain so the rules we will add,
|
||||||
# which rely on translating/filtering packets on lo, can work
|
# which rely on translating/filtering packets on lo, can work
|
||||||
if self.has_skip_loopback():
|
if self.has_skip_loopback():
|
||||||
pfctl('-f /dev/stdin', b'pass on lo\n')
|
pfctl('-f /dev/stdin', b'pass on lo\n')
|
||||||
super(Darwin, self).add_anchors(anchor)
|
super(Darwin, self).add_anchors(anchor)
|
||||||
@ -386,17 +385,18 @@ else:
|
|||||||
|
|
||||||
def pfctl(args, stdin=None):
|
def pfctl(args, stdin=None):
|
||||||
argv = ['pfctl'] + shlex.split(args)
|
argv = ['pfctl'] + shlex.split(args)
|
||||||
debug1('>> %s' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
||||||
stdout=ssubprocess.PIPE,
|
stdout=ssubprocess.PIPE,
|
||||||
stderr=ssubprocess.PIPE,
|
stderr=ssubprocess.PIPE,
|
||||||
env=get_env())
|
env=env)
|
||||||
o = p.communicate(stdin)
|
o = p.communicate(stdin)
|
||||||
if p.returncode:
|
if p.returncode:
|
||||||
log('%r returned %d, stdout and stderr follows: ' %
|
|
||||||
(argv, p.returncode))
|
|
||||||
log("stdout:\n%s" % o[0].decode("ascii"))
|
|
||||||
log("stderr:\n%s" % o[1].decode("ascii"))
|
|
||||||
raise Fatal('%r returned %d' % (argv, p.returncode))
|
raise Fatal('%r returned %d' % (argv, p.returncode))
|
||||||
|
|
||||||
return o
|
return o
|
||||||
@ -448,7 +448,7 @@ class Method(BaseMethod):
|
|||||||
return sock.getsockname()
|
return sock.getsockname()
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user, group, tmark):
|
user):
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by pf method_name'
|
'Address family "%s" unsupported by pf method_name'
|
||||||
@ -473,7 +473,7 @@ class Method(BaseMethod):
|
|||||||
pf.add_rules(anchor, includes, port, dnsport, nslist, family)
|
pf.add_rules(anchor, includes, port, dnsport, nslist, family)
|
||||||
pf.enable()
|
pf.enable()
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user, group):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by pf method_name'
|
'Address family "%s" unsupported by pf method_name'
|
||||||
@ -495,9 +495,3 @@ class Method(BaseMethod):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_supported(self):
|
|
||||||
if which("pfctl"):
|
|
||||||
return True
|
|
||||||
debug2("pf method not supported because 'pfctl' command is missing.")
|
|
||||||
return False
|
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
import struct
|
import struct
|
||||||
from sshuttle.firewall import subnet_weight
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import family_to_string
|
from sshuttle.helpers import family_to_string
|
||||||
from sshuttle.linux import ipt, ipt_chain_exists
|
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
from sshuttle.helpers import debug1, debug2, debug3, Fatal, which
|
from sshuttle.helpers import debug1, debug3, Fatal
|
||||||
|
|
||||||
import socket
|
recvmsg = None
|
||||||
import os
|
try:
|
||||||
|
# try getting recvmsg from python
|
||||||
|
import socket as pythonsocket
|
||||||
|
getattr(pythonsocket.socket, "recvmsg")
|
||||||
|
socket = pythonsocket
|
||||||
|
recvmsg = "python"
|
||||||
|
except AttributeError:
|
||||||
|
# try getting recvmsg from socket_ext library
|
||||||
|
try:
|
||||||
|
import socket_ext
|
||||||
|
getattr(socket_ext.socket, "recvmsg")
|
||||||
|
socket = socket_ext
|
||||||
|
recvmsg = "socket_ext"
|
||||||
|
except ImportError:
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
IP_TRANSPARENT = 19
|
IP_TRANSPARENT = 19
|
||||||
@ -16,37 +30,75 @@ SOL_IPV6 = 41
|
|||||||
IPV6_ORIGDSTADDR = 74
|
IPV6_ORIGDSTADDR = 74
|
||||||
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
||||||
|
|
||||||
|
if recvmsg == "python":
|
||||||
def recv_udp(listener, bufsize):
|
def recv_udp(listener, bufsize):
|
||||||
debug3('Accept UDP python using recvmsg.')
|
debug3('Accept UDP python using recvmsg.\n')
|
||||||
data, ancdata, _, srcip = listener.recvmsg(
|
data, ancdata, _, srcip = listener.recvmsg(
|
||||||
4096, socket.CMSG_SPACE(24))
|
4096, socket.CMSG_SPACE(24))
|
||||||
dstip = None
|
dstip = None
|
||||||
family = None
|
family = None
|
||||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
|
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
|
||||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||||
port = socket.htons(port)
|
port = socket.htons(port)
|
||||||
if family == socket.AF_INET:
|
if family == socket.AF_INET:
|
||||||
start = 4
|
start = 4
|
||||||
length = 4
|
length = 4
|
||||||
else:
|
else:
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||||
dstip = (ip, port)
|
dstip = (ip, port)
|
||||||
break
|
break
|
||||||
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
|
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
|
||||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||||
port = socket.htons(port)
|
port = socket.htons(port)
|
||||||
if family == socket.AF_INET6:
|
if family == socket.AF_INET6:
|
||||||
start = 8
|
start = 8
|
||||||
length = 16
|
length = 16
|
||||||
else:
|
else:
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||||
dstip = (ip, port)
|
dstip = (ip, port)
|
||||||
break
|
break
|
||||||
return (srcip, dstip, data)
|
return (srcip, dstip, data)
|
||||||
|
elif recvmsg == "socket_ext":
|
||||||
|
def recv_udp(listener, bufsize):
|
||||||
|
debug3('Accept UDP using socket_ext recvmsg.\n')
|
||||||
|
srcip, data, adata, _ = listener.recvmsg(
|
||||||
|
(bufsize,), socket.CMSG_SPACE(24))
|
||||||
|
dstip = None
|
||||||
|
family = None
|
||||||
|
for a in adata:
|
||||||
|
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
|
||||||
|
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
||||||
|
port = socket.htons(port)
|
||||||
|
if family == socket.AF_INET:
|
||||||
|
start = 4
|
||||||
|
length = 4
|
||||||
|
else:
|
||||||
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
|
ip = socket.inet_ntop(
|
||||||
|
family, a.cmsg_data[start:start + length])
|
||||||
|
dstip = (ip, port)
|
||||||
|
break
|
||||||
|
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
|
||||||
|
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
||||||
|
port = socket.htons(port)
|
||||||
|
if family == socket.AF_INET6:
|
||||||
|
start = 8
|
||||||
|
length = 16
|
||||||
|
else:
|
||||||
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
|
ip = socket.inet_ntop(
|
||||||
|
family, a.cmsg_data[start:start + length])
|
||||||
|
dstip = (ip, port)
|
||||||
|
break
|
||||||
|
return (srcip, dstip, data[0])
|
||||||
|
else:
|
||||||
|
def recv_udp(listener, bufsize):
|
||||||
|
debug3('Accept UDP using recvfrom.\n')
|
||||||
|
data, srcip = listener.recvfrom(bufsize)
|
||||||
|
return (srcip, None, data)
|
||||||
|
|
||||||
|
|
||||||
class Method(BaseMethod):
|
class Method(BaseMethod):
|
||||||
@ -54,8 +106,12 @@ class Method(BaseMethod):
|
|||||||
def get_supported_features(self):
|
def get_supported_features(self):
|
||||||
result = super(Method, self).get_supported_features()
|
result = super(Method, self).get_supported_features()
|
||||||
result.ipv6 = True
|
result.ipv6 = True
|
||||||
result.udp = True
|
if recvmsg is None:
|
||||||
result.dns = True
|
result.udp = False
|
||||||
|
result.dns = False
|
||||||
|
else:
|
||||||
|
result.udp = True
|
||||||
|
result.dns = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_tcp_dstip(self, sock):
|
def get_tcp_dstip(self, sock):
|
||||||
@ -70,15 +126,6 @@ class Method(BaseMethod):
|
|||||||
return None
|
return None
|
||||||
return srcip, dstip, data
|
return srcip, dstip, data
|
||||||
|
|
||||||
def setsockopt_error(self, e):
|
|
||||||
"""The tproxy method needs root permissions to successfully
|
|
||||||
set the IP_TRANSPARENT option on sockets. This method is
|
|
||||||
called when we receive a PermissionError when trying to do
|
|
||||||
so."""
|
|
||||||
raise Fatal("Insufficient permissions for tproxy method.\n"
|
|
||||||
"Your effective UID is %d, not 0. Try rerunning as root.\n"
|
|
||||||
% os.geteuid())
|
|
||||||
|
|
||||||
def send_udp(self, sock, srcip, dstip, data):
|
def send_udp(self, sock, srcip, dstip, data):
|
||||||
if not srcip:
|
if not srcip:
|
||||||
debug1(
|
debug1(
|
||||||
@ -87,26 +134,16 @@ class Method(BaseMethod):
|
|||||||
return
|
return
|
||||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
try:
|
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
|
||||||
except PermissionError as e:
|
|
||||||
self.setsockopt_error(e)
|
|
||||||
sender.bind(srcip)
|
sender.bind(srcip)
|
||||||
sender.sendto(data, dstip)
|
sender.sendto(data, dstip)
|
||||||
sender.close()
|
sender.close()
|
||||||
|
|
||||||
def setup_tcp_listener(self, tcp_listener):
|
def setup_tcp_listener(self, tcp_listener):
|
||||||
try:
|
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
|
||||||
except PermissionError as e:
|
|
||||||
self.setsockopt_error(e)
|
|
||||||
|
|
||||||
def setup_udp_listener(self, udp_listener):
|
def setup_udp_listener(self, udp_listener):
|
||||||
try:
|
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
|
||||||
except PermissionError as e:
|
|
||||||
self.setsockopt_error(e)
|
|
||||||
|
|
||||||
if udp_listener.v4 is not None:
|
if udp_listener.v4 is not None:
|
||||||
udp_listener.v4.setsockopt(
|
udp_listener.v4.setsockopt(
|
||||||
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
||||||
@ -114,7 +151,7 @@ class Method(BaseMethod):
|
|||||||
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user, group, tmark):
|
user):
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by tproxy method'
|
'Address family "%s" unsupported by tproxy method'
|
||||||
@ -125,16 +162,19 @@ class Method(BaseMethod):
|
|||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
|
def _ipt_ttl(*args):
|
||||||
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
def _ipt_proto_ports(proto, fport, lport):
|
def _ipt_proto_ports(proto, fport, lport):
|
||||||
return proto + ('--dport', '%d:%d' % (fport, lport)) \
|
return proto + ('--dport', '%d:%d' % (fport, lport)) \
|
||||||
if fport else proto
|
if fport else proto
|
||||||
|
|
||||||
mark_chain = 'sshuttle-m-%s' % port
|
mark_chain = 'sshuttle-m-%s' % port
|
||||||
tproxy_chain = 'sshuttle-t-%s' % port
|
tproxy_chain = 'sshuttle-t-%s' % port
|
||||||
divert_chain = 'sshuttle-d-%s' % port
|
divert_chain = 'sshuttle-d-%s' % port
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
self.restore_firewall(port, family, udp, user, group)
|
self.restore_firewall(port, family, udp, user)
|
||||||
|
|
||||||
_ipt('-N', mark_chain)
|
_ipt('-N', mark_chain)
|
||||||
_ipt('-F', mark_chain)
|
_ipt('-F', mark_chain)
|
||||||
@ -144,33 +184,7 @@ class Method(BaseMethod):
|
|||||||
_ipt('-F', tproxy_chain)
|
_ipt('-F', tproxy_chain)
|
||||||
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
|
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
|
||||||
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
|
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
|
||||||
|
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
|
||||||
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', divert_chain, '-j', 'ACCEPT')
|
||||||
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
'-m', 'tcp', '-p', 'tcp')
|
||||||
@ -179,6 +193,16 @@ class Method(BaseMethod):
|
|||||||
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||||
'-m', 'udp', '-p', 'udp')
|
'-m', 'udp', '-p', 'udp')
|
||||||
|
|
||||||
|
for _, ip in [i for i in nslist if i[0] == family]:
|
||||||
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
|
'--dest', '%s/32' % ip,
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
||||||
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1',
|
||||||
|
'--dest', '%s/32' % ip,
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
||||||
|
'--on-port', str(dnsport))
|
||||||
|
|
||||||
for _, swidth, sexclude, snet, fport, lport \
|
for _, swidth, sexclude, snet, fport, lport \
|
||||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||||
tcp_ports = ('-p', 'tcp')
|
tcp_ports = ('-p', 'tcp')
|
||||||
@ -194,12 +218,12 @@ class Method(BaseMethod):
|
|||||||
'-m', 'tcp',
|
'-m', 'tcp',
|
||||||
*tcp_ports)
|
*tcp_ports)
|
||||||
else:
|
else:
|
||||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'tcp',
|
'-m', 'tcp',
|
||||||
*tcp_ports)
|
*tcp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
'--tproxy-mark', tmark,
|
'--tproxy-mark', '0x1/0x1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'tcp',
|
'-m', 'tcp',
|
||||||
*(tcp_ports + ('--on-port', str(port))))
|
*(tcp_ports + ('--on-port', str(port))))
|
||||||
@ -218,17 +242,17 @@ class Method(BaseMethod):
|
|||||||
'-m', 'udp',
|
'-m', 'udp',
|
||||||
*udp_ports)
|
*udp_ports)
|
||||||
else:
|
else:
|
||||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'udp',
|
'-m', 'udp',
|
||||||
*udp_ports)
|
*udp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
'--tproxy-mark', tmark,
|
'--tproxy-mark', '0x1/0x1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'udp',
|
'-m', 'udp',
|
||||||
*(udp_ports + ('--on-port', str(port))))
|
*(udp_ports + ('--on-port', str(port))))
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user, group):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by tproxy method'
|
'Address family "%s" unsupported by tproxy method'
|
||||||
@ -239,6 +263,9 @@ class Method(BaseMethod):
|
|||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
|
def _ipt_ttl(*args):
|
||||||
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
mark_chain = 'sshuttle-m-%s' % port
|
mark_chain = 'sshuttle-m-%s' % port
|
||||||
tproxy_chain = 'sshuttle-t-%s' % port
|
tproxy_chain = 'sshuttle-t-%s' % port
|
||||||
divert_chain = 'sshuttle-d-%s' % port
|
divert_chain = 'sshuttle-d-%s' % port
|
||||||
@ -257,10 +284,3 @@ class Method(BaseMethod):
|
|||||||
if ipt_chain_exists(family, table, divert_chain):
|
if ipt_chain_exists(family, table, divert_chain):
|
||||||
_ipt('-F', divert_chain)
|
_ipt('-F', divert_chain)
|
||||||
_ipt('-X', divert_chain)
|
_ipt('-X', divert_chain)
|
||||||
|
|
||||||
def is_supported(self):
|
|
||||||
if which("iptables") and which("ip6tables"):
|
|
||||||
return True
|
|
||||||
debug2("tproxy method not supported because 'iptables' "
|
|
||||||
"or 'ip6tables' commands are missing.\n")
|
|
||||||
return False
|
|
||||||
|
@ -1,533 +0,0 @@
|
|||||||
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()
|
|
@ -1,40 +0,0 @@
|
|||||||
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)
|
|
@ -1,6 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import sys
|
|
||||||
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
|
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
|
||||||
|
|
||||||
from sshuttle import __version__
|
from sshuttle import __version__
|
||||||
@ -29,73 +28,29 @@ def parse_subnetport_file(s):
|
|||||||
# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
|
# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
|
||||||
# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
|
# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
|
||||||
# example.com:123 or just example.com
|
# example.com:123 or just example.com
|
||||||
#
|
|
||||||
# In addition, the port number can be specified as a range:
|
|
||||||
# 1.2.3.4:8000-8080.
|
|
||||||
#
|
|
||||||
# Can return multiple matches if the domain name used in the request
|
|
||||||
# has multiple IP addresses.
|
|
||||||
def parse_subnetport(s):
|
def parse_subnetport(s):
|
||||||
|
|
||||||
if s.count(':') > 1:
|
if s.count(':') > 1:
|
||||||
rx = r'(?:\[?(?:\*\.)?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
|
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
|
||||||
else:
|
else:
|
||||||
rx = r'((?:\*\.)?[\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
|
rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
|
||||||
|
|
||||||
m = re.match(rx, s)
|
m = re.match(rx, s)
|
||||||
if not m:
|
if not m:
|
||||||
raise Fatal('%r is not a valid address/mask:port format' % s)
|
raise Fatal('%r is not a valid address/mask:port format' % s)
|
||||||
|
|
||||||
# Ports range from fport to lport. If only one port is specified,
|
addr, width, fport, lport = m.groups()
|
||||||
# fport is defined and lport is None.
|
|
||||||
#
|
|
||||||
# cidr is the mask defined with the slash notation
|
|
||||||
host, cidr, fport, lport = m.groups()
|
|
||||||
try:
|
try:
|
||||||
addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM)
|
addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
raise Fatal('Unable to resolve address: %s' % host)
|
raise Fatal('Unable to resolve address: %s' % addr)
|
||||||
|
|
||||||
# If the address is a domain with multiple IPs and a mask is also
|
family, _, _, _, addr = min(addrinfo)
|
||||||
# provided, proceed cautiously:
|
max_width = 32 if family == socket.AF_INET else 128
|
||||||
if cidr is not None:
|
width = int(width or max_width)
|
||||||
addr_v6 = [a for a in addrinfo if a[0] == socket.AF_INET6]
|
if not 0 <= width <= max_width:
|
||||||
addr_v4 = [a for a in addrinfo if a[0] == socket.AF_INET]
|
raise Fatal('width %d is not between 0 and %d' % (width, max_width))
|
||||||
|
|
||||||
# Refuse to proceed if IPv4 and IPv6 addresses are present:
|
return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
|
||||||
if len(addr_v6) > 0 and len(addr_v4) > 0:
|
|
||||||
raise Fatal("%s has IPv4 and IPv6 addresses, so the mask "
|
|
||||||
"of /%s is not supported. Specify the IP "
|
|
||||||
"addresses directly if you wish to specify "
|
|
||||||
"a mask." % (host, cidr))
|
|
||||||
|
|
||||||
# Warn if a domain has multiple IPs of the same type (IPv4 vs
|
|
||||||
# IPv6) and the mask is applied to all of the IPs.
|
|
||||||
if len(addr_v4) > 1 or len(addr_v6) > 1:
|
|
||||||
print("WARNING: %s has multiple IP addresses. The "
|
|
||||||
"mask of /%s is applied to all of the addresses."
|
|
||||||
% (host, cidr))
|
|
||||||
|
|
||||||
rv = []
|
|
||||||
for a in addrinfo:
|
|
||||||
family, _, _, _, addr = a
|
|
||||||
|
|
||||||
# Largest possible slash value we can use with this IP:
|
|
||||||
max_cidr = 32 if family == socket.AF_INET else 128
|
|
||||||
|
|
||||||
if cidr is None: # if no mask, use largest mask
|
|
||||||
cidr_to_use = max_cidr
|
|
||||||
else: # verify user-provided mask is appropriate
|
|
||||||
cidr_to_use = int(cidr)
|
|
||||||
if not 0 <= cidr_to_use <= max_cidr:
|
|
||||||
raise Fatal('Slash in CIDR notation (/%d) is '
|
|
||||||
'not between 0 and %d'
|
|
||||||
% (cidr_to_use, max_cidr))
|
|
||||||
|
|
||||||
rv.append((family, addr[0], cidr_to_use,
|
|
||||||
int(fport or 0), int(lport or fport or 0)))
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
||||||
@ -114,38 +69,23 @@ def parse_ipport(s):
|
|||||||
if not m:
|
if not m:
|
||||||
raise Fatal('%r is not a valid IP:port format' % s)
|
raise Fatal('%r is not a valid IP:port format' % s)
|
||||||
|
|
||||||
host, port = m.groups()
|
ip, port = m.groups()
|
||||||
host = host or '0.0.0.0'
|
ip = ip or '0.0.0.0'
|
||||||
port = int(port or 0)
|
port = int(port or 0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
|
addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
raise Fatal('Unable to resolve address: %s' % host)
|
raise Fatal('%r is not a valid IP:port format' % s)
|
||||||
|
|
||||||
if len(addrinfo) > 1:
|
|
||||||
print("WARNING: Host %s has more than one IP, only using one of them."
|
|
||||||
% host)
|
|
||||||
|
|
||||||
family, _, _, _, addr = min(addrinfo)
|
family, _, _, _, addr = min(addrinfo)
|
||||||
# Note: addr contains (ip, port)
|
|
||||||
return (family,) + addr[:2]
|
return (family,) + addr[:2]
|
||||||
|
|
||||||
|
|
||||||
def parse_list(lst):
|
def parse_list(lst):
|
||||||
"""Parse a comma separated string into a list."""
|
|
||||||
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
||||||
|
|
||||||
|
|
||||||
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):
|
class Concat(Action):
|
||||||
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||||
if nargs is not None:
|
if nargs is not None:
|
||||||
@ -157,33 +97,9 @@ class Concat(Action):
|
|||||||
setattr(namespace, self.dest, curr_value + values)
|
setattr(namespace, self.dest, curr_value + values)
|
||||||
|
|
||||||
|
|
||||||
# Override one function in the ArgumentParser so that we can have
|
parser = ArgumentParser(
|
||||||
# better control for how we parse files containing arguments. We
|
|
||||||
# expect one argument per line, but strip whitespace/quotes from the
|
|
||||||
# beginning/end of the lines.
|
|
||||||
class MyArgumentParser(ArgumentParser):
|
|
||||||
def convert_arg_line_to_args(self, arg_line):
|
|
||||||
# Ignore comments
|
|
||||||
if arg_line.startswith("#"):
|
|
||||||
return []
|
|
||||||
|
|
||||||
# strip whitespace at beginning and end of line
|
|
||||||
arg_line = arg_line.strip()
|
|
||||||
|
|
||||||
# When copying parameters from the command line to a file,
|
|
||||||
# some users might copy the quotes they used on the command
|
|
||||||
# line into the config file. We ignore these if the line
|
|
||||||
# starts and ends with the same quote.
|
|
||||||
if arg_line.startswith("'") and arg_line.endswith("'") or \
|
|
||||||
arg_line.startswith('"') and arg_line.endswith('"'):
|
|
||||||
arg_line = arg_line[1:-1]
|
|
||||||
|
|
||||||
return [arg_line]
|
|
||||||
|
|
||||||
|
|
||||||
parser = MyArgumentParser(
|
|
||||||
prog="sshuttle",
|
prog="sshuttle",
|
||||||
usage="%(prog)s [-l [ip:]port] -r [user@]sshserver[:port] <subnets...>",
|
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
|
||||||
fromfile_prefix_chars="@"
|
fromfile_prefix_chars="@"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -231,7 +147,6 @@ parser.add_argument(
|
|||||||
type=parse_list,
|
type=parse_list,
|
||||||
help="""
|
help="""
|
||||||
capture and forward DNS requests made to the following servers
|
capture and forward DNS requests made to the following servers
|
||||||
(comma separated)
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -244,14 +159,9 @@ parser.add_argument(
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
method_choices = ["auto", "windivert"]
|
|
||||||
else:
|
|
||||||
method_choices = ["auto", "nft", "nat", "tproxy", "pf", "ipfw"]
|
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--method",
|
"--method",
|
||||||
choices=method_choices,
|
choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
|
||||||
metavar="TYPE",
|
metavar="TYPE",
|
||||||
default="auto",
|
default="auto",
|
||||||
help="""
|
help="""
|
||||||
@ -297,7 +207,7 @@ parser.add_argument(
|
|||||||
action="count",
|
action="count",
|
||||||
default=0,
|
default=0,
|
||||||
help="""
|
help="""
|
||||||
increase debug message verbosity (can be used more than once)
|
increase debug message verbosity
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -316,22 +226,6 @@ parser.add_argument(
|
|||||||
the command to use to connect to the remote [%(default)s]
|
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(
|
parser.add_argument(
|
||||||
"--seed-hosts",
|
"--seed-hosts",
|
||||||
metavar="HOSTNAME[,HOSTNAME]",
|
metavar="HOSTNAME[,HOSTNAME]",
|
||||||
@ -413,12 +307,6 @@ parser.add_argument(
|
|||||||
apply all the rules only to this linux user
|
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(
|
parser.add_argument(
|
||||||
"--firewall",
|
"--firewall",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@ -433,15 +321,18 @@ parser.add_argument(
|
|||||||
(internal use only)
|
(internal use only)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sudoers",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
Add sshuttle to the sudoers for this user
|
||||||
|
"""
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--sudoers-no-modify",
|
"--sudoers-no-modify",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="""
|
help="""
|
||||||
Prints a sudo configuration to STDOUT which allows a user to
|
Prints the sudoers config to STDOUT and DOES NOT modify anything.
|
||||||
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(
|
parser.add_argument(
|
||||||
@ -449,7 +340,16 @@ parser.add_argument(
|
|||||||
default="",
|
default="",
|
||||||
help="""
|
help="""
|
||||||
Set the user name or group with %%group_name for passwordless operation.
|
Set the user name or group with %%group_name for passwordless operation.
|
||||||
Default is the current user. Only works with the --sudoers-no-modify option.
|
Default is the current user.set ALL for all users. Only works with
|
||||||
|
--sudoers or --sudoers-no-modify option.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sudoers-filename",
|
||||||
|
default="sshuttle_auto",
|
||||||
|
help="""
|
||||||
|
Set the file name for the sudoers.d file to be added. Default is
|
||||||
|
"sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option.
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -460,29 +360,3 @@ parser.add_argument(
|
|||||||
do not set PYTHONPATH when invoking sudo
|
do not set PYTHONPATH when invoking sudo
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"-t", "--tmark",
|
|
||||||
metavar="[MARK]",
|
|
||||||
default="0x01",
|
|
||||||
help="""
|
|
||||||
tproxy optional traffic mark with provided MARK value in
|
|
||||||
hexadecimal (default '0x01')
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
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."""
|
|
||||||
)
|
|
||||||
|
@ -1,13 +1,3 @@
|
|||||||
"""When sshuttle is run via a systemd service file, we can communicate
|
|
||||||
to systemd about the status of the sshuttle process. In particular, we
|
|
||||||
can send READY status to tell systemd that sshuttle has completed
|
|
||||||
startup and send STOPPING to indicate that sshuttle is beginning
|
|
||||||
shutdown.
|
|
||||||
|
|
||||||
For details, see:
|
|
||||||
https://www.freedesktop.org/software/systemd/man/sd_notify.html
|
|
||||||
"""
|
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -15,7 +5,6 @@ from sshuttle.helpers import debug1
|
|||||||
|
|
||||||
|
|
||||||
def _notify(message):
|
def _notify(message):
|
||||||
"""Send a notification message to systemd."""
|
|
||||||
addr = os.environ.get("NOTIFY_SOCKET", None)
|
addr = os.environ.get("NOTIFY_SOCKET", None)
|
||||||
|
|
||||||
if not addr or len(addr) == 1 or addr[0] not in ('/', '@'):
|
if not addr or len(addr) == 1 or addr[0] not in ('/', '@'):
|
||||||
@ -26,7 +15,7 @@ def _notify(message):
|
|||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
debug1("Error creating socket to notify systemd: %s" % e)
|
debug1("Error creating socket to notify systemd: %s\n" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
@ -37,27 +26,21 @@ def _notify(message):
|
|||||||
try:
|
try:
|
||||||
return (sock.sendto(message, addr) > 0)
|
return (sock.sendto(message, addr) > 0)
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
debug1("Error notifying systemd: %s" % e)
|
debug1("Error notifying systemd: %s\n" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def send(*messages):
|
def send(*messages):
|
||||||
"""Send multiple messages to systemd."""
|
|
||||||
return _notify(b'\n'.join(messages))
|
return _notify(b'\n'.join(messages))
|
||||||
|
|
||||||
|
|
||||||
def ready():
|
def ready():
|
||||||
"""Constructs a message that is appropriate to send upon completion of
|
|
||||||
sshuttle startup."""
|
|
||||||
return b"READY=1"
|
return b"READY=1"
|
||||||
|
|
||||||
|
|
||||||
def stop():
|
def stop():
|
||||||
"""Constructs a message that is appropriate to send when sshuttle is
|
|
||||||
beginning to shutdown."""
|
|
||||||
return b"STOPPING=1"
|
return b"STOPPING=1"
|
||||||
|
|
||||||
|
|
||||||
def status(message):
|
def status(message):
|
||||||
"""Constructs a status message to be sent to systemd."""
|
|
||||||
return b"STATUS=%s" % message.encode('utf8')
|
return b"STATUS=%s" % message.encode('utf8')
|
||||||
|
@ -5,8 +5,8 @@ import traceback
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import io
|
import platform
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
import sshuttle.ssnet as ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
@ -14,7 +14,7 @@ import sshuttle.hostwatch as hostwatch
|
|||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
||||||
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
|
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
|
||||||
get_random_nameserver, which, get_env, SocketRWShim
|
resolvconf_random_nameserver
|
||||||
|
|
||||||
|
|
||||||
def _ipmatch(ipstr):
|
def _ipmatch(ipstr):
|
||||||
@ -35,6 +35,7 @@ def _ipmatch(ipstr):
|
|||||||
elif g[3] is None:
|
elif g[3] is None:
|
||||||
ips += '.0'
|
ips += '.0'
|
||||||
width = min(width, 24)
|
width = min(width, 24)
|
||||||
|
ips = ips
|
||||||
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
||||||
|
|
||||||
|
|
||||||
@ -79,23 +80,13 @@ def _route_iproute(line):
|
|||||||
return ipw, int(mask)
|
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):
|
def _list_routes(argv, extract_route):
|
||||||
# FIXME: IPv4 only
|
# FIXME: IPv4 only
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||||
routes = []
|
routes = []
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
if not line.strip():
|
if not line.strip():
|
||||||
@ -109,23 +100,20 @@ def _list_routes(argv, extract_route):
|
|||||||
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
|
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
if rv != 0:
|
if rv != 0:
|
||||||
log('WARNING: %r returned %d' % (argv, rv))
|
log('WARNING: %r returned %d\n' % (argv, rv))
|
||||||
|
log('WARNING: That prevents --auto-nets from working.\n')
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
|
|
||||||
def list_routes():
|
def list_routes():
|
||||||
if sys.platform == 'win32':
|
if which('ip'):
|
||||||
routes = _list_routes(['route', 'PRINT', '-4'], _route_windows)
|
routes = _list_routes(['ip', 'route'], _route_iproute)
|
||||||
|
elif which('netstat'):
|
||||||
|
routes = _list_routes(['netstat', '-rn'], _route_netstat)
|
||||||
else:
|
else:
|
||||||
if which('ip'):
|
log('WARNING: Neither ip nor netstat were found on the server.\n')
|
||||||
routes = _list_routes(['ip', 'route'], _route_iproute)
|
routes = []
|
||||||
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:
|
for (family, ip, width) in routes:
|
||||||
if not ip.startswith('0.') and not ip.startswith('127.'):
|
if not ip.startswith('0.') and not ip.startswith('127.'):
|
||||||
@ -151,7 +139,7 @@ def start_hostwatch(seed_hosts, auto_hosts):
|
|||||||
s1.close()
|
s1.close()
|
||||||
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
|
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
log('%s' % _exc_dump())
|
log('%s\n' % _exc_dump())
|
||||||
rv = 98
|
rv = 98
|
||||||
finally:
|
finally:
|
||||||
os._exit(rv)
|
os._exit(rv)
|
||||||
@ -199,7 +187,7 @@ class DnsProxy(Handler):
|
|||||||
self.tries += 1
|
self.tries += 1
|
||||||
|
|
||||||
if self.to_nameserver is None:
|
if self.to_nameserver is None:
|
||||||
_, peer = get_random_nameserver()
|
_, peer = resolvconf_random_nameserver()
|
||||||
port = 53
|
port = 53
|
||||||
else:
|
else:
|
||||||
peer = self.to_ns_peer
|
peer = self.to_ns_peer
|
||||||
@ -207,11 +195,12 @@ class DnsProxy(Handler):
|
|||||||
|
|
||||||
family, sockaddr = self._addrinfo(peer, port)
|
family, sockaddr = self._addrinfo(peer, port)
|
||||||
sock = socket.socket(family, socket.SOCK_DGRAM)
|
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
sock.connect(sockaddr)
|
sock.connect(sockaddr)
|
||||||
|
|
||||||
self.peers[sock] = peer
|
self.peers[sock] = peer
|
||||||
|
|
||||||
debug2('DNS: sending to %r:%d (try %d)' % (peer, port, self.tries))
|
debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries))
|
||||||
try:
|
try:
|
||||||
sock.send(self.request)
|
sock.send(self.request)
|
||||||
self.socks.append(sock)
|
self.socks.append(sock)
|
||||||
@ -221,11 +210,11 @@ class DnsProxy(Handler):
|
|||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
# and sometimes by send(). We have to catch both.
|
# and sometimes by send(). We have to catch both.
|
||||||
debug2('DNS send to %r: %s' % (peer, e))
|
debug2('DNS send to %r: %s\n' % (peer, e))
|
||||||
self.try_send()
|
self.try_send()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
log('DNS send to %r: %s' % (peer, e))
|
log('DNS send to %r: %s\n' % (peer, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
@ -242,13 +231,13 @@ class DnsProxy(Handler):
|
|||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
# and sometimes by send(). We have to catch both.
|
# and sometimes by send(). We have to catch both.
|
||||||
debug2('DNS recv from %r: %s' % (peer, e))
|
debug2('DNS recv from %r: %s\n' % (peer, e))
|
||||||
self.try_send()
|
self.try_send()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
log('DNS recv from %r: %s' % (peer, e))
|
log('DNS recv from %r: %s\n' % (peer, e))
|
||||||
return
|
return
|
||||||
debug2('DNS response: %d bytes' % len(data))
|
debug2('DNS response: %d bytes\n' % len(data))
|
||||||
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
||||||
self.ok = False
|
self.ok = False
|
||||||
|
|
||||||
@ -262,14 +251,16 @@ class UdpProxy(Handler):
|
|||||||
self.mux = mux
|
self.mux = mux
|
||||||
self.chan = chan
|
self.chan = chan
|
||||||
self.sock = sock
|
self.sock = sock
|
||||||
|
if family == socket.AF_INET:
|
||||||
|
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
|
|
||||||
def send(self, dstip, data):
|
def send(self, dstip, data):
|
||||||
debug2('UDP: sending to %r port %d' % dstip)
|
debug2('UDP: sending to %r port %d\n' % dstip)
|
||||||
try:
|
try:
|
||||||
self.sock.sendto(data, dstip)
|
self.sock.sendto(data, dstip)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log('UDP send to %r port %d: %s' % (dstip[0], dstip[1], e))
|
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
@ -277,166 +268,150 @@ class UdpProxy(Handler):
|
|||||||
data, peer = sock.recvfrom(4096)
|
data, peer = sock.recvfrom(4096)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log('UDP recv from %r port %d: %s' % (peer[0], peer[1], e))
|
log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
|
||||||
return
|
return
|
||||||
debug2('UDP response: %d bytes' % len(data))
|
debug2('UDP response: %d bytes\n' % len(data))
|
||||||
hdr = b("%s,%r," % (peer[0], peer[1]))
|
hdr = b("%s,%r," % (peer[0], peer[1]))
|
||||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||||
|
|
||||||
|
|
||||||
def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
|
def main(latency_control, auto_hosts, to_nameserver, auto_nets):
|
||||||
auto_nets):
|
debug1('Starting server with Python version %s\n'
|
||||||
try:
|
% platform.python_version())
|
||||||
|
|
||||||
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = ' s: '
|
helpers.logprefix = ' s: '
|
||||||
|
else:
|
||||||
|
helpers.logprefix = 'server: '
|
||||||
|
debug1('latency control setting = %r\n' % latency_control)
|
||||||
|
|
||||||
debug1('latency control setting = %r' % latency_control)
|
# synchronization header
|
||||||
if latency_buffer_size:
|
sys.stdout.write('\0\0SSHUTTLE0001')
|
||||||
import sshuttle.ssnet as ssnet
|
sys.stdout.flush()
|
||||||
ssnet.LATENCY_BUFFER_SIZE = latency_buffer_size
|
|
||||||
|
|
||||||
# synchronization header
|
handlers = []
|
||||||
sys.stdout.write('\0\0SSHUTTLE0001')
|
mux = Mux(sys.stdin, sys.stdout)
|
||||||
sys.stdout.flush()
|
handlers.append(mux)
|
||||||
|
|
||||||
handlers = []
|
debug1('auto-nets:' + str(auto_nets) + '\n')
|
||||||
# get unbuffered stdin and stdout in binary mode. Equivalent to stdin.buffer/stdout.buffer (Only available in Python 3)
|
if auto_nets:
|
||||||
r, w = io.FileIO(0, mode='r'), io.FileIO(1, mode='w')
|
routes = list(list_routes())
|
||||||
if sys.platform == 'win32':
|
debug1('available routes:\n')
|
||||||
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))
|
|
||||||
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:
|
for r in routes:
|
||||||
routepkt += '%d,%s,%d\n' % r
|
debug1(' %d/%s/%d\n' % r)
|
||||||
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
|
else:
|
||||||
|
routes = []
|
||||||
|
|
||||||
hw = Hostwatch()
|
routepkt = ''
|
||||||
hw.leftover = b('')
|
for r in routes:
|
||||||
|
routepkt += '%d,%s,%d\n' % r
|
||||||
|
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
|
||||||
|
|
||||||
def hostwatch_ready(sock):
|
hw = Hostwatch()
|
||||||
assert hw.pid
|
hw.leftover = b('')
|
||||||
content = hw.sock.recv(4096)
|
|
||||||
if content:
|
def hostwatch_ready(sock):
|
||||||
lines = (hw.leftover + content).split(b('\n'))
|
assert(hw.pid)
|
||||||
if lines[-1]:
|
content = hw.sock.recv(4096)
|
||||||
# no terminating newline: entry isn't complete yet!
|
if content:
|
||||||
hw.leftover = lines.pop()
|
lines = (hw.leftover + content).split(b('\n'))
|
||||||
lines.append(b(''))
|
if lines[-1]:
|
||||||
else:
|
# no terminating newline: entry isn't complete yet!
|
||||||
hw.leftover = b('')
|
hw.leftover = lines.pop()
|
||||||
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
|
lines.append(b(''))
|
||||||
else:
|
else:
|
||||||
raise Fatal('hostwatch process died')
|
hw.leftover = b('')
|
||||||
|
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
|
||||||
|
else:
|
||||||
|
raise Fatal('hostwatch process died')
|
||||||
|
|
||||||
def got_host_req(data):
|
def got_host_req(data):
|
||||||
if not hw.pid:
|
if not hw.pid:
|
||||||
(hw.pid, hw.sock) = start_hostwatch(
|
(hw.pid, hw.sock) = start_hostwatch(
|
||||||
data.decode("ASCII").strip().split(), auto_hosts)
|
data.decode("ASCII").strip().split(), auto_hosts)
|
||||||
handlers.append(Handler(socks=[hw.sock],
|
handlers.append(Handler(socks=[hw.sock],
|
||||||
callback=hostwatch_ready))
|
callback=hostwatch_ready))
|
||||||
mux.got_host_req = got_host_req
|
mux.got_host_req = got_host_req
|
||||||
|
|
||||||
def new_channel(channel, data):
|
def new_channel(channel, data):
|
||||||
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
|
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
|
||||||
family = int(family)
|
family = int(family)
|
||||||
# AF_INET is the same constant on Linux and BSD but AF_INET6
|
# AF_INET is the same constant on Linux and BSD but AF_INET6
|
||||||
# is different. As the client and server can be running on
|
# is different. As the client and server can be running on
|
||||||
# different platforms we can not just set the socket family
|
# different platforms we can not just set the socket family
|
||||||
# to what comes in the wire.
|
# to what comes in the wire.
|
||||||
if family != socket.AF_INET:
|
if family != socket.AF_INET:
|
||||||
family = socket.AF_INET6
|
family = socket.AF_INET6
|
||||||
|
dstport = int(dstport)
|
||||||
|
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
||||||
|
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||||
|
mux.new_channel = new_channel
|
||||||
|
|
||||||
|
dnshandlers = {}
|
||||||
|
|
||||||
|
def dns_req(channel, data):
|
||||||
|
debug2('Incoming DNS request channel=%d.\n' % channel)
|
||||||
|
h = DnsProxy(mux, channel, data, to_nameserver)
|
||||||
|
handlers.append(h)
|
||||||
|
dnshandlers[channel] = h
|
||||||
|
mux.got_dns_req = dns_req
|
||||||
|
|
||||||
|
udphandlers = {}
|
||||||
|
|
||||||
|
def udp_req(channel, cmd, data):
|
||||||
|
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
|
||||||
|
if cmd == ssnet.CMD_UDP_DATA:
|
||||||
|
(dstip, dstport, data) = data.split(b(','), 2)
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
|
||||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
h = udphandlers[channel]
|
||||||
mux.new_channel = new_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]
|
||||||
|
|
||||||
dnshandlers = {}
|
def udp_open(channel, data):
|
||||||
|
debug2('Incoming UDP open.\n')
|
||||||
def dns_req(channel, data):
|
family = int(data)
|
||||||
debug2('Incoming DNS request channel=%d.' % channel)
|
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
|
||||||
h = DnsProxy(mux, channel, data, to_nameserver)
|
if channel in udphandlers:
|
||||||
|
raise Fatal('UDP connection channel %d already open' % channel)
|
||||||
|
else:
|
||||||
|
h = UdpProxy(mux, channel, family)
|
||||||
handlers.append(h)
|
handlers.append(h)
|
||||||
dnshandlers[channel] = h
|
udphandlers[channel] = h
|
||||||
mux.got_dns_req = dns_req
|
mux.got_udp_open = udp_open
|
||||||
|
|
||||||
udphandlers = {}
|
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)
|
||||||
|
|
||||||
def udp_req(channel, cmd, data):
|
ssnet.runonce(handlers, mux)
|
||||||
debug2('Incoming UDP request channel=%d, cmd=%d' %
|
if latency_control:
|
||||||
(channel, cmd))
|
mux.check_fullness()
|
||||||
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]
|
|
||||||
|
|
||||||
def udp_open(channel, data):
|
if dnshandlers:
|
||||||
debug2('Incoming UDP open.')
|
now = time.time()
|
||||||
family = int(data)
|
remove = []
|
||||||
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd,
|
for channel, h in dnshandlers.items():
|
||||||
data)
|
if h.timeout < now or not h.ok:
|
||||||
if channel in udphandlers:
|
debug3('expiring dnsreqs channel=%d\n' % channel)
|
||||||
raise Fatal('UDP connection channel %d already open' %
|
remove.append(channel)
|
||||||
channel)
|
h.ok = False
|
||||||
else:
|
for channel in remove:
|
||||||
h = UdpProxy(mux, channel, family)
|
del dnshandlers[channel]
|
||||||
handlers.append(h)
|
if udphandlers:
|
||||||
udphandlers[channel] = h
|
remove = []
|
||||||
mux.got_udp_open = udp_open
|
for channel, h in udphandlers.items():
|
||||||
|
if not h.ok:
|
||||||
while mux.ok:
|
debug3('expiring UDP channel=%d\n' % channel)
|
||||||
if hw.pid:
|
remove.append(channel)
|
||||||
assert hw.pid > 0
|
h.ok = False
|
||||||
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
for channel in remove:
|
||||||
if rpid:
|
del udphandlers[channel]
|
||||||
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)
|
|
||||||
|
220
sshuttle/ssh.py
220
sshuttle/ssh.py
@ -3,8 +3,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import zlib
|
import zlib
|
||||||
import importlib
|
import imp
|
||||||
import importlib.util
|
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
import shlex
|
import shlex
|
||||||
from shlex import quote
|
from shlex import quote
|
||||||
@ -12,18 +11,46 @@ import ipaddress
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
from sshuttle.helpers import debug2, which, get_path, SocketRWShim, Fatal
|
from sshuttle.helpers import debug2
|
||||||
|
|
||||||
|
|
||||||
def get_module_source(name):
|
def readfile(name):
|
||||||
spec = importlib.util.find_spec(name)
|
tokens = name.split(".")
|
||||||
with open(spec.origin, "rt") as f:
|
f = None
|
||||||
return f.read().encode("utf-8")
|
|
||||||
|
token = tokens[0]
|
||||||
|
token_name = [token]
|
||||||
|
token_str = ".".join(token_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
f, pathname, description = imp.find_module(token_str)
|
||||||
|
|
||||||
|
for token in tokens[1:]:
|
||||||
|
module = imp.load_module(token_str, f, pathname, description)
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
token_name.append(token)
|
||||||
|
token_str = ".".join(token_name)
|
||||||
|
|
||||||
|
f, pathname, description = imp.find_module(
|
||||||
|
token, module.__path__)
|
||||||
|
|
||||||
|
if f is not None:
|
||||||
|
contents = f.read()
|
||||||
|
else:
|
||||||
|
contents = ""
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return contents.encode("UTF8")
|
||||||
|
|
||||||
|
|
||||||
def empackage(z, name, data=None):
|
def empackage(z, name, data=None):
|
||||||
if not data:
|
if not data:
|
||||||
data = get_module_source(name)
|
data = readfile(name)
|
||||||
content = z.compress(data)
|
content = z.compress(data)
|
||||||
content += z.flush(zlib.Z_SYNC_FLUSH)
|
content += z.flush(zlib.Z_SYNC_FLUSH)
|
||||||
|
|
||||||
@ -41,38 +68,36 @@ def parse_hostport(rhostport):
|
|||||||
|
|
||||||
and returns a tuple (username, password, port, host)
|
and returns a tuple (username, password, port, host)
|
||||||
"""
|
"""
|
||||||
# leave use of default port to ssh command to prevent overwriting
|
# default port for SSH is TCP port 22
|
||||||
# ports configured in ~/.ssh/config when no port is given
|
port = 22
|
||||||
if rhostport is None or len(rhostport) == 0:
|
|
||||||
return None, None, None, None
|
|
||||||
port = None
|
|
||||||
username = None
|
username = None
|
||||||
password = None
|
password = None
|
||||||
host = rhostport
|
host = rhostport
|
||||||
|
|
||||||
if "@" in host:
|
if "@" in host:
|
||||||
# split username (and possible password) from the host[:port]
|
# split username (and possible password) from the host[:port]
|
||||||
username, host = host.rsplit("@", 1)
|
username, host = host.split("@")
|
||||||
# Fix #410 bad username error detect
|
# Fix #410 bad username error detect
|
||||||
|
# username cannot contain an @ sign in this scenario
|
||||||
if ":" in username:
|
if ":" in username:
|
||||||
# this will even allow for the username to be empty
|
# this will even allow for the username to be empty
|
||||||
username, password = username.split(":", 1)
|
username, password = username.split(":")
|
||||||
|
|
||||||
if ":" in host:
|
if ":" in host:
|
||||||
# IPv6 address and/or got a port specified
|
# IPv6 address and/or got a port specified
|
||||||
|
|
||||||
# If it is an IPv6 address with port specification,
|
# If it is an IPv6 adress with port specification,
|
||||||
# then it will look like: [::1]:22
|
# then it will look like: [::1]:22
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# try to parse host as an IP address,
|
# try to parse host as an IP adress,
|
||||||
# if that works it is an IPv6 address
|
# if that works it is an IPv6 address
|
||||||
host = str(ipaddress.ip_address(host))
|
host = ipaddress.ip_address(host)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# if that fails parse as URL to get the port
|
# if that fails parse as URL to get the port
|
||||||
parsed = urlparse('//{}'.format(host))
|
parsed = urlparse('//{}'.format(host))
|
||||||
try:
|
try:
|
||||||
host = str(ipaddress.ip_address(parsed.hostname))
|
host = ipaddress.ip_address(parsed.hostname)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# else if both fails, we have a hostname with port
|
# else if both fails, we have a hostname with port
|
||||||
host = parsed.hostname
|
host = parsed.hostname
|
||||||
@ -84,7 +109,7 @@ def parse_hostport(rhostport):
|
|||||||
return username, password, port, host
|
return username, password, port, host
|
||||||
|
|
||||||
|
|
||||||
def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell, options):
|
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||||
username, password, port, host = parse_hostport(rhostport)
|
username, password, port, host = parse_hostport(rhostport)
|
||||||
if username:
|
if username:
|
||||||
rhost = "{}@{}".format(username, host)
|
rhost = "{}@{}".format(username, host)
|
||||||
@ -92,7 +117,7 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
|
|||||||
rhost = host
|
rhost = host
|
||||||
|
|
||||||
z = zlib.compressobj(1)
|
z = zlib.compressobj(1)
|
||||||
content = get_module_source('sshuttle.assembler')
|
content = readfile('sshuttle.assembler')
|
||||||
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
|
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
|
||||||
optdata = optdata.encode("UTF8")
|
optdata = optdata.encode("UTF8")
|
||||||
content2 = (empackage(z, 'sshuttle') +
|
content2 = (empackage(z, 'sshuttle') +
|
||||||
@ -103,21 +128,11 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
|
|||||||
empackage(z, 'sshuttle.server') +
|
empackage(z, 'sshuttle.server') +
|
||||||
b"\n")
|
b"\n")
|
||||||
|
|
||||||
# If the exec() program calls sys.exit(), it should exit python
|
|
||||||
# and the sys.exit(98) call won't be reached (so we try to only
|
|
||||||
# exit that way in the server). However, if the code that we
|
|
||||||
# exec() simply returns from main, then we will return from
|
|
||||||
# exec(). If the server's python process dies, it should stop
|
|
||||||
# executing and also won't reach sys.exit(98).
|
|
||||||
#
|
|
||||||
# So, we shouldn't reach sys.exit(98) and we certainly shouldn't
|
|
||||||
# reach it immediately after trying to start the server.
|
|
||||||
pyscript = r"""
|
pyscript = r"""
|
||||||
import sys, os;
|
import sys, os;
|
||||||
verbosity=%d;
|
verbosity=%d;
|
||||||
stdin = os.fdopen(0, 'rb');
|
sys.stdin = os.fdopen(0, "rb");
|
||||||
exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
|
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
|
||||||
sys.exit(98);
|
|
||||||
""" % (helpers.verbose or 0, len(content))
|
""" % (helpers.verbose or 0, len(content))
|
||||||
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
||||||
|
|
||||||
@ -130,126 +145,35 @@ def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell,
|
|||||||
sshl = shlex.split(ssh_cmd)
|
sshl = shlex.split(ssh_cmd)
|
||||||
else:
|
else:
|
||||||
sshl = ['ssh']
|
sshl = ['ssh']
|
||||||
if port is not None:
|
if python:
|
||||||
portl = ["-p", str(port)]
|
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
||||||
else:
|
else:
|
||||||
portl = []
|
pycmd = ("P=python3; $P -V 2>%s || P=python; "
|
||||||
if remote_shell == "cmd":
|
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript))
|
||||||
pycmd = '"%s" -c "%s"' % (python or 'python', pyscript)
|
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
|
||||||
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:
|
if password is not None:
|
||||||
os.environ['SSHPASS'] = str(password)
|
os.environ['SSHPASS'] = str(password)
|
||||||
argv = (["sshpass", "-e"] + sshl +
|
argv = (["sshpass", "-e"] + sshl +
|
||||||
portl + [rhost])
|
["-p", str(port)] +
|
||||||
|
[rhost, '--', pycmd])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
argv = (sshl + portl + [rhost])
|
argv = (sshl +
|
||||||
|
["-p", str(port)] +
|
||||||
|
[rhost, '--', pycmd])
|
||||||
|
(s1, s2) = socket.socketpair()
|
||||||
|
|
||||||
if add_cmd_delimiter:
|
def setup():
|
||||||
argv += ['--', pycmd]
|
# runs in the child process
|
||||||
else:
|
s2.close()
|
||||||
argv += [pycmd]
|
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
|
||||||
|
s1.close()
|
||||||
# Our which() function searches for programs in get_path()
|
debug2('executing: %r\n' % argv)
|
||||||
# directories (which include PATH). This step isn't strictly
|
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
||||||
# necessary if ssh is already in the user's PATH, but it makes the
|
close_fds=True, stderr=stderr)
|
||||||
# error message friendlier if the user incorrectly passes in a
|
os.close(s1a)
|
||||||
# custom ssh command that we cannot find.
|
os.close(s1b)
|
||||||
abs_path = which(argv[0])
|
s2.sendall(content)
|
||||||
if abs_path is None:
|
s2.sendall(content2)
|
||||||
raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path()))
|
return p, s2
|
||||||
argv[0] = abs_path
|
|
||||||
|
|
||||||
if sys.platform != 'win32':
|
|
||||||
(s1, s2) = socket.socketpair()
|
|
||||||
pstdin, pstdout = os.dup(s1.fileno()), os.dup(s1.fileno())
|
|
||||||
|
|
||||||
def preexec_fn():
|
|
||||||
# runs in the child process
|
|
||||||
s2.close()
|
|
||||||
s1.close()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
@ -5,17 +5,20 @@ import errno
|
|||||||
import select
|
import select
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, set_non_blocking_io
|
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
|
||||||
|
|
||||||
MAX_CHANNEL = 65535
|
MAX_CHANNEL = 65535
|
||||||
LATENCY_BUFFER_SIZE = 32768
|
LATENCY_BUFFER_SIZE = 32768
|
||||||
|
|
||||||
|
# these don't exist in the socket module in python 2.3!
|
||||||
SHUT_RD = 0
|
SHUT_RD = 0
|
||||||
SHUT_WR = 1
|
SHUT_WR = 1
|
||||||
SHUT_RDWR = 2
|
SHUT_RDWR = 2
|
||||||
|
|
||||||
|
|
||||||
HDR_LEN = 8
|
HDR_LEN = 8
|
||||||
|
|
||||||
|
|
||||||
CMD_EXIT = 0x4200
|
CMD_EXIT = 0x4200
|
||||||
CMD_PING = 0x4201
|
CMD_PING = 0x4201
|
||||||
CMD_PONG = 0x4202
|
CMD_PONG = 0x4202
|
||||||
@ -77,13 +80,12 @@ def _fds(socks):
|
|||||||
def _nb_clean(func, *args):
|
def _nb_clean(func, *args):
|
||||||
try:
|
try:
|
||||||
return func(*args)
|
return func(*args)
|
||||||
except (OSError, socket.error):
|
except OSError:
|
||||||
# Note: In python2 socket.error != OSError (In python3, they are same)
|
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
debug3('%s: err was: %s' % (func.__name__, e))
|
debug3('%s: err was: %s\n' % (func.__name__, e))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -111,7 +113,7 @@ class SockWrapper:
|
|||||||
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
||||||
global _swcount
|
global _swcount
|
||||||
_swcount += 1
|
_swcount += 1
|
||||||
debug3('creating new SockWrapper (%d now exist)' % _swcount)
|
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
|
||||||
self.exc = None
|
self.exc = None
|
||||||
self.rsock = rsock
|
self.rsock = rsock
|
||||||
self.wsock = wsock
|
self.wsock = wsock
|
||||||
@ -124,9 +126,9 @@ class SockWrapper:
|
|||||||
def __del__(self):
|
def __del__(self):
|
||||||
global _swcount
|
global _swcount
|
||||||
_swcount -= 1
|
_swcount -= 1
|
||||||
debug1('%r: deleting (%d remain)' % (self, _swcount))
|
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
|
||||||
if self.exc:
|
if self.exc:
|
||||||
debug1('%r: error was: %s' % (self, self.exc))
|
debug1('%r: error was: %s\n' % (self, self.exc))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.rsock == self.wsock:
|
if self.rsock == self.wsock:
|
||||||
@ -148,14 +150,14 @@ class SockWrapper:
|
|||||||
if not self.connect_to:
|
if not self.connect_to:
|
||||||
return # already connected
|
return # already connected
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
debug3('%r: trying connect to %r' % (self, self.connect_to))
|
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
|
||||||
try:
|
try:
|
||||||
self.rsock.connect(self.connect_to)
|
self.rsock.connect(self.connect_to)
|
||||||
# connected successfully (Linux)
|
# connected successfully (Linux)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
debug3('%r: connect result: %s' % (self, e))
|
debug3('%r: connect result: %s\n' % (self, e))
|
||||||
if e.args[0] == errno.EINVAL:
|
if e.args[0] == errno.EINVAL:
|
||||||
# this is what happens when you call connect() on a socket
|
# this is what happens when you call connect() on a socket
|
||||||
# that is now connected but returned EINPROGRESS last time,
|
# that is now connected but returned EINPROGRESS last time,
|
||||||
@ -165,28 +167,22 @@ class SockWrapper:
|
|||||||
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
|
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
|
||||||
socket.SO_ERROR)
|
socket.SO_ERROR)
|
||||||
e = socket.error(realerr, os.strerror(realerr))
|
e = socket.error(realerr, os.strerror(realerr))
|
||||||
debug3('%r: fixed connect result: %s' % (self, e))
|
debug3('%r: fixed connect result: %s\n' % (self, e))
|
||||||
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
||||||
pass # not connected yet
|
pass # not connected yet
|
||||||
elif sys.platform == 'win32' and e.args[0] == errno.WSAEWOULDBLOCK: # 10035
|
|
||||||
pass # not connected yet
|
|
||||||
elif e.args[0] == 0:
|
elif e.args[0] == 0:
|
||||||
if sys.platform == 'win32':
|
# connected successfully (weird Linux bug?)
|
||||||
# On Windows "real" error of EINVAL could be 0, when socket is in connecting state
|
# Sometimes Linux seems to return EINVAL when it isn't
|
||||||
pass
|
# invalid. This *may* be caused by a race condition
|
||||||
else:
|
# between connect() and getsockopt(SO_ERROR) (ie. it
|
||||||
# connected successfully (weird Linux bug?)
|
# finishes connecting in between the two, so there is no
|
||||||
# Sometimes Linux seems to return EINVAL when it isn't
|
# longer an error). However, I'm not sure of that.
|
||||||
# invalid. This *may* be caused by a race condition
|
#
|
||||||
# between connect() and getsockopt(SO_ERROR) (ie. it
|
# I did get at least one report that the problem went away
|
||||||
# finishes connecting in between the two, so there is no
|
# when we added this, however.
|
||||||
# longer an error). However, I'm not sure of that.
|
self.connect_to = None
|
||||||
#
|
|
||||||
# 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:
|
elif e.args[0] == errno.EISCONN:
|
||||||
# connected successfully (BSD + Windows)
|
# connected successfully (BSD)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]:
|
elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]:
|
||||||
# a "normal" kind of error
|
# a "normal" kind of error
|
||||||
@ -197,12 +193,13 @@ class SockWrapper:
|
|||||||
|
|
||||||
def noread(self):
|
def noread(self):
|
||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
debug2('%r: done reading' % self)
|
debug2('%r: done reading\n' % self)
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
|
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
|
||||||
|
|
||||||
def nowrite(self):
|
def nowrite(self):
|
||||||
if not self.shut_write:
|
if not self.shut_write:
|
||||||
debug2('%r: done writing' % self)
|
debug2('%r: done writing\n' % self)
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
try:
|
try:
|
||||||
self.wsock.shutdown(SHUT_WR)
|
self.wsock.shutdown(SHUT_WR)
|
||||||
@ -219,11 +216,11 @@ class SockWrapper:
|
|||||||
return 0 # still connecting
|
return 0 # still connecting
|
||||||
self.wsock.setblocking(False)
|
self.wsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(self.wsock.send, buf)
|
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
||||||
except OSError:
|
except OSError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.EPIPE:
|
if e.errno == errno.EPIPE:
|
||||||
debug1('%r: uwrite: got EPIPE' % self)
|
debug1('%r: uwrite: got EPIPE\n' % self)
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
@ -232,7 +229,7 @@ class SockWrapper:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
def write(self, buf):
|
def write(self, buf):
|
||||||
assert buf
|
assert(buf)
|
||||||
return self.uwrite(buf)
|
return self.uwrite(buf)
|
||||||
|
|
||||||
def uread(self):
|
def uread(self):
|
||||||
@ -242,7 +239,7 @@ class SockWrapper:
|
|||||||
return
|
return
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(self.rsock.recv, 65536)
|
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
||||||
except OSError:
|
except OSError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
self.seterr('uread: %s' % e)
|
self.seterr('uread: %s' % e)
|
||||||
@ -280,12 +277,12 @@ class Handler:
|
|||||||
_add(r, i)
|
_add(r, i)
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
log('--no callback defined-- %r' % self)
|
log('--no callback defined-- %r\n' % self)
|
||||||
(r, _, _) = select.select(self.socks, [], [], 0)
|
(r, _, _) = select.select(self.socks, [], [], 0)
|
||||||
for s in r:
|
for s in r:
|
||||||
v = s.recv(4096)
|
v = s.recv(4096)
|
||||||
if not v:
|
if not v:
|
||||||
log('--closed-- %r' % self)
|
log('--closed-- %r\n' % self)
|
||||||
self.socks = []
|
self.socks = []
|
||||||
self.ok = False
|
self.ok = False
|
||||||
|
|
||||||
@ -378,6 +375,11 @@ class Mux(Handler):
|
|||||||
if not self.too_full:
|
if not self.too_full:
|
||||||
self.send(0, CMD_PING, b('rttest'))
|
self.send(0, CMD_PING, b('rttest'))
|
||||||
self.too_full = True
|
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):
|
def send(self, channel, cmd, data):
|
||||||
assert isinstance(data, bytes)
|
assert isinstance(data, bytes)
|
||||||
@ -385,34 +387,32 @@ class Mux(Handler):
|
|||||||
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
|
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
|
||||||
+ data
|
+ data
|
||||||
self.outbuf.append(p)
|
self.outbuf.append(p)
|
||||||
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
|
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
||||||
len(data), self.fullness))
|
len(data), self.fullness))
|
||||||
# debug3('>>> data: %r' % data)
|
|
||||||
self.fullness += len(data)
|
self.fullness += len(data)
|
||||||
|
|
||||||
def got_packet(self, channel, cmd, data):
|
def got_packet(self, channel, cmd, data):
|
||||||
debug2('< channel=%d cmd=%s len=%d'
|
debug2('< channel=%d cmd=%s len=%d\n'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
||||||
# debug3('<<< data: %r' % data)
|
|
||||||
if cmd == CMD_PING:
|
if cmd == CMD_PING:
|
||||||
self.send(0, CMD_PONG, data)
|
self.send(0, CMD_PONG, data)
|
||||||
elif cmd == CMD_PONG:
|
elif cmd == CMD_PONG:
|
||||||
debug2('received PING response')
|
debug2('received PING response\n')
|
||||||
self.too_full = False
|
self.too_full = False
|
||||||
self.fullness = 0
|
self.fullness = 0
|
||||||
elif cmd == CMD_EXIT:
|
elif cmd == CMD_EXIT:
|
||||||
self.ok = False
|
self.ok = False
|
||||||
elif cmd == CMD_TCP_CONNECT:
|
elif cmd == CMD_TCP_CONNECT:
|
||||||
assert not self.channels.get(channel)
|
assert(not self.channels.get(channel))
|
||||||
if self.new_channel:
|
if self.new_channel:
|
||||||
self.new_channel(channel, data)
|
self.new_channel(channel, data)
|
||||||
elif cmd == CMD_DNS_REQ:
|
elif cmd == CMD_DNS_REQ:
|
||||||
assert not self.channels.get(channel)
|
assert(not self.channels.get(channel))
|
||||||
if self.got_dns_req:
|
if self.got_dns_req:
|
||||||
self.got_dns_req(channel, data)
|
self.got_dns_req(channel, data)
|
||||||
elif cmd == CMD_UDP_OPEN:
|
elif cmd == CMD_UDP_OPEN:
|
||||||
assert not self.channels.get(channel)
|
assert(not self.channels.get(channel))
|
||||||
if self.got_udp_open:
|
if self.got_udp_open:
|
||||||
self.got_udp_open(channel, data)
|
self.got_udp_open(channel, data)
|
||||||
elif cmd == CMD_ROUTES:
|
elif cmd == CMD_ROUTES:
|
||||||
@ -433,33 +433,29 @@ class Mux(Handler):
|
|||||||
else:
|
else:
|
||||||
callback = self.channels.get(channel)
|
callback = self.channels.get(channel)
|
||||||
if not callback:
|
if not callback:
|
||||||
log('warning: closed channel %d got cmd=%s len=%d'
|
log('warning: closed channel %d got cmd=%s len=%d\n'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
||||||
else:
|
else:
|
||||||
callback(cmd, data)
|
callback(cmd, data)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
set_non_blocking_io(self.wfile.fileno())
|
os.set_blocking(self.wfile.fileno(), False)
|
||||||
if self.outbuf and self.outbuf[0]:
|
if self.outbuf and self.outbuf[0]:
|
||||||
wrote = _nb_clean(self.wfile.write, self.outbuf[0])
|
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
|
||||||
# self.wfile.flush()
|
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
|
||||||
debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0])))
|
|
||||||
if wrote:
|
if wrote:
|
||||||
self.outbuf[0] = self.outbuf[0][wrote:]
|
self.outbuf[0] = self.outbuf[0][wrote:]
|
||||||
while self.outbuf and not self.outbuf[0]:
|
while self.outbuf and not self.outbuf[0]:
|
||||||
self.outbuf[0:1] = []
|
self.outbuf[0:1] = []
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
set_non_blocking_io(self.rfile.fileno())
|
os.set_blocking(self.rfile.fileno(), False)
|
||||||
try:
|
try:
|
||||||
# If LATENCY_BUFFER_SIZE is inappropriately large, we will
|
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
|
||||||
# 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:
|
except OSError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
raise Fatal('other end: %r' % e)
|
raise Fatal('other end: %r' % e)
|
||||||
# log('<<< %r' % b)
|
# log('<<< %r\n' % b)
|
||||||
if read == b(''): # EOF
|
if read == b(''): # EOF
|
||||||
self.ok = False
|
self.ok = False
|
||||||
if read:
|
if read:
|
||||||
@ -467,12 +463,14 @@ class Mux(Handler):
|
|||||||
|
|
||||||
def handle(self):
|
def handle(self):
|
||||||
self.fill()
|
self.fill()
|
||||||
|
# log('inbuf is: (%d,%d) %r\n'
|
||||||
|
# % (self.want, len(self.inbuf), self.inbuf))
|
||||||
while 1:
|
while 1:
|
||||||
if len(self.inbuf) >= (self.want or HDR_LEN):
|
if len(self.inbuf) >= (self.want or HDR_LEN):
|
||||||
(s1, s2, channel, cmd, datalen) = \
|
(s1, s2, channel, cmd, datalen) = \
|
||||||
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
||||||
assert s1 == b('S')
|
assert(s1 == b('S'))
|
||||||
assert s2 == b('S')
|
assert(s2 == b('S'))
|
||||||
self.want = datalen + HDR_LEN
|
self.want = datalen + HDR_LEN
|
||||||
if self.want and len(self.inbuf) >= self.want:
|
if self.want and len(self.inbuf) >= self.want:
|
||||||
data = self.inbuf[HDR_LEN:self.want]
|
data = self.inbuf[HDR_LEN:self.want]
|
||||||
@ -503,7 +501,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.mux.channels[channel] = self.got_packet
|
self.mux.channels[channel] = self.got_packet
|
||||||
self.socks = []
|
self.socks = []
|
||||||
debug2('new channel: %d' % channel)
|
debug2('new channel: %d\n' % channel)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
@ -519,7 +517,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def setnoread(self):
|
def setnoread(self):
|
||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
debug2('%r: done reading' % self)
|
debug2('%r: done reading\n' % self)
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
@ -530,13 +528,13 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def setnowrite(self):
|
def setnowrite(self):
|
||||||
if not self.shut_write:
|
if not self.shut_write:
|
||||||
debug2('%r: done writing' % self)
|
debug2('%r: done writing\n' % self)
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
def maybe_close(self):
|
def maybe_close(self):
|
||||||
if self.shut_read and self.shut_write:
|
if self.shut_read and self.shut_write:
|
||||||
debug2('%r: closing connection' % self)
|
debug2('%r: closing connection\n' % self)
|
||||||
# remove the mux's reference to us. The python garbage collector
|
# remove the mux's reference to us. The python garbage collector
|
||||||
# will then be able to reap our object.
|
# will then be able to reap our object.
|
||||||
self.mux.channels[self.channel] = None
|
self.mux.channels[self.channel] = None
|
||||||
@ -573,9 +571,9 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
|
|
||||||
def connect_dst(family, ip, port):
|
def connect_dst(family, ip, port):
|
||||||
debug2('Connecting to %s:%d' % (ip, port))
|
debug2('Connecting to %s:%d\n' % (ip, port))
|
||||||
outsock = socket.socket(family)
|
outsock = socket.socket(family)
|
||||||
|
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
return SockWrapper(outsock, outsock,
|
return SockWrapper(outsock, outsock,
|
||||||
connect_to=(ip, port),
|
connect_to=(ip, port),
|
||||||
peername='%s:%d' % (ip, port))
|
peername='%s:%d' % (ip, port))
|
||||||
@ -591,11 +589,11 @@ def runonce(handlers, mux):
|
|||||||
|
|
||||||
for s in handlers:
|
for s in handlers:
|
||||||
s.pre_select(r, w, x)
|
s.pre_select(r, w, x)
|
||||||
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)'
|
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
|
||||||
% (len(handlers), _fds(r), _fds(w), _fds(x),
|
% (len(handlers), _fds(r), _fds(w), _fds(x),
|
||||||
mux.fullness, mux.too_full))
|
mux.fullness, mux.too_full))
|
||||||
(r, w, x) = select.select(r, w, x)
|
(r, w, x) = select.select(r, w, x)
|
||||||
debug2(' Ready: %d r=%r w=%r x=%r'
|
debug2(' Ready: %d r=%r w=%r x=%r\n'
|
||||||
% (len(handlers), _fds(r), _fds(w), _fds(x)))
|
% (len(handlers), _fds(r), _fds(w), _fds(x)))
|
||||||
ready = r + w + x
|
ready = r + w + x
|
||||||
did = {}
|
did = {}
|
||||||
|
@ -10,7 +10,7 @@ def start_syslog():
|
|||||||
global _p
|
global _p
|
||||||
with open(os.devnull, 'w') as devnull:
|
with open(os.devnull, 'w') as devnull:
|
||||||
_p = ssubprocess.Popen(
|
_p = ssubprocess.Popen(
|
||||||
['logger', '-p', 'daemon.err', '-t', 'sshuttle'],
|
['logger', '-p', 'daemon.notice', '-t', 'sshuttle'],
|
||||||
stdin=ssubprocess.PIPE,
|
stdin=ssubprocess.PIPE,
|
||||||
stdout=devnull,
|
stdout=devnull,
|
||||||
stderr=devnull
|
stderr=devnull
|
||||||
|
89
sshuttle/stresstest.py
Executable file
89
sshuttle/stresstest.py
Executable file
@ -0,0 +1,89 @@
|
|||||||
|
#!/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()
|
@ -2,44 +2,63 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import getpass
|
import getpass
|
||||||
from uuid import uuid4
|
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):
|
def build_config(user_name):
|
||||||
"""Generates a sudoers configuration to allow passwordless execution of sshuttle."""
|
content = template % {
|
||||||
|
'ca': command_alias,
|
||||||
|
'dist_packages': path_to_dist_packages,
|
||||||
|
'py': sys.executable,
|
||||||
|
'path': path_to_sshuttle,
|
||||||
|
'user_name': user_name,
|
||||||
|
}
|
||||||
|
|
||||||
argv0 = os.path.abspath(sys.argv[0])
|
return content
|
||||||
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 sudoers(user_name=None):
|
def save_config(content, file_name):
|
||||||
|
process = Popen([
|
||||||
|
'/usr/bin/sudo',
|
||||||
|
spawn.find_executable('sudoers-add'),
|
||||||
|
file_name,
|
||||||
|
], stdout=PIPE, stdin=PIPE)
|
||||||
|
|
||||||
|
process.stdin.write(content.encode())
|
||||||
|
|
||||||
|
streamdata = process.communicate()[0]
|
||||||
|
returncode = process.returncode
|
||||||
|
|
||||||
|
if returncode:
|
||||||
|
log('Failed updating sudoers file.\n')
|
||||||
|
debug1(streamdata)
|
||||||
|
exit(returncode)
|
||||||
|
else:
|
||||||
|
log('Success, sudoers file update.\n')
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def sudoers(user_name=None, no_modify=None, file_name=None):
|
||||||
user_name = user_name or getpass.getuser()
|
user_name = user_name or getpass.getuser()
|
||||||
content = build_config(user_name)
|
content = build_config(user_name)
|
||||||
sys.stdout.write(content)
|
|
||||||
exit(0)
|
if no_modify:
|
||||||
|
sys.stdout.write(content)
|
||||||
|
exit(0)
|
||||||
|
else:
|
||||||
|
save_config(content, file_name)
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
|
||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
from unittest.mock import Mock, patch, call
|
from mock import Mock, patch, call
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import sshuttle.firewall
|
import sshuttle.firewall
|
||||||
|
|
||||||
|
|
||||||
def setup_daemon():
|
def setup_daemon():
|
||||||
stdin = io.BytesIO(u"""ROUTES
|
stdin = io.StringIO(u"""ROUTES
|
||||||
{inet},24,0,1.2.3.0,8000,9000
|
{inet},24,0,1.2.3.0,8000,9000
|
||||||
{inet},32,1,1.2.3.66,8080,8080
|
{inet},32,1,1.2.3.66,8080,8080
|
||||||
{inet6},64,0,2404:6800:4004:80c::,0,0
|
{inet6},64,0,2404:6800:4004:80c::,0,0
|
||||||
@ -19,9 +15,9 @@ NSLIST
|
|||||||
{inet},1.2.3.33
|
{inet},1.2.3.33
|
||||||
{inet6},2404:6800:4004:80c::33
|
{inet6},2404:6800:4004:80c::33
|
||||||
PORTS 1024,1025,1026,1027
|
PORTS 1024,1025,1026,1027
|
||||||
GO 1 - - 0x01 12345
|
GO 1 -
|
||||||
HOST 1.2.3.3,existing
|
HOST 1.2.3.3,existing
|
||||||
""".format(inet=AF_INET, inet6=AF_INET6).encode('ASCII'))
|
""".format(inet=AF_INET, inet6=AF_INET6))
|
||||||
stdout = Mock()
|
stdout = Mock()
|
||||||
return stdin, stdout
|
return stdin, stdout
|
||||||
|
|
||||||
@ -59,25 +55,10 @@ def test_rewrite_etc_hosts(tmpdir):
|
|||||||
assert line == ""
|
assert line == ""
|
||||||
|
|
||||||
with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
|
with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
|
||||||
sshuttle.firewall.restore_etc_hosts(hostmap, 10)
|
sshuttle.firewall.restore_etc_hosts(10)
|
||||||
assert orig_hosts.computehash() == new_hosts.computehash()
|
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():
|
def test_subnet_weight():
|
||||||
subnets = [
|
subnets = [
|
||||||
(AF_INET, 16, 0, '192.168.0.0', 0, 0),
|
(AF_INET, 16, 0, '192.168.0.0', 0, 0),
|
||||||
@ -127,37 +108,30 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
|
|||||||
]
|
]
|
||||||
|
|
||||||
assert stdout.mock_calls == [
|
assert stdout.mock_calls == [
|
||||||
call.write(b'READY test\n'),
|
call.write('READY test\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write(b'STARTED\n'),
|
call.write('STARTED\n'),
|
||||||
call.flush()
|
call.flush()
|
||||||
]
|
]
|
||||||
assert mock_setup_daemon.mock_calls == [call()]
|
assert mock_setup_daemon.mock_calls == [call()]
|
||||||
assert mock_get_method.mock_calls == [
|
assert mock_get_method.mock_calls == [
|
||||||
call('not_auto'),
|
call('not_auto'),
|
||||||
call().is_supported(),
|
|
||||||
call().is_supported().__bool__(),
|
|
||||||
call().setup_firewall(
|
call().setup_firewall(
|
||||||
1024, 1026,
|
1024, 1026,
|
||||||
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
AF_INET6,
|
AF_INET6,
|
||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None,
|
None),
|
||||||
None,
|
|
||||||
'0x01'),
|
|
||||||
call().setup_firewall(
|
call().setup_firewall(
|
||||||
1025, 1027,
|
1025, 1027,
|
||||||
[(AF_INET, u'1.2.3.33')],
|
[(AF_INET, u'1.2.3.33')],
|
||||||
AF_INET,
|
AF_INET,
|
||||||
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
True,
|
True,
|
||||||
None,
|
None),
|
||||||
None,
|
call().restore_firewall(1024, AF_INET6, True, None),
|
||||||
'0x01'),
|
call().restore_firewall(1025, AF_INET, True, None),
|
||||||
call().wait_for_firewall_ready(12345),
|
|
||||||
call().restore_firewall(1024, AF_INET6, True, None, None),
|
|
||||||
call().restore_firewall(1025, AF_INET, True, None, None),
|
|
||||||
]
|
]
|
||||||
|
@ -3,7 +3,7 @@ import socket
|
|||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
import errno
|
import errno
|
||||||
|
|
||||||
from unittest.mock import patch, call
|
from mock import patch, call
|
||||||
import sshuttle.helpers
|
import sshuttle.helpers
|
||||||
|
|
||||||
|
|
||||||
@ -24,19 +24,19 @@ def test_log(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message\n'),
|
call.write('prefix: message'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: abc\n'),
|
call.write('prefix: abc'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: message 1\n'),
|
call.write('prefix: message 1\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: message 2\n'),
|
call.write('prefix: message 2\n'),
|
||||||
call.write(' line2\n'),
|
call.write('---> line2\n'),
|
||||||
call.write(' line3\n'),
|
call.write('---> line3\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: message 3\n'),
|
call.write('prefix: message 3\n'),
|
||||||
call.write(' line2\n'),
|
call.write('---> line2\n'),
|
||||||
call.write(' line3\n'),
|
call.write('---> line3\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message\n'),
|
call.write('prefix: message'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message\n'),
|
call.write('prefix: message'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message\n'),
|
call.write('prefix: message'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ nameserver 2404:6800:4004:80c::3
|
|||||||
nameserver 2404:6800:4004:80c::4
|
nameserver 2404:6800:4004:80c::4
|
||||||
""")
|
""")
|
||||||
|
|
||||||
ns = sshuttle.helpers.resolvconf_nameservers(False)
|
ns = sshuttle.helpers.resolvconf_nameservers()
|
||||||
assert ns == [
|
assert ns == [
|
||||||
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
||||||
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
||||||
@ -143,7 +143,7 @@ nameserver 2404:6800:4004:80c::4
|
|||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.helpers.open', create=True)
|
@patch('sshuttle.helpers.open', create=True)
|
||||||
def test_get_random_nameserver(mock_open):
|
def test_resolvconf_random_nameserver(mock_open):
|
||||||
mock_open.return_value = io.StringIO(u"""
|
mock_open.return_value = io.StringIO(u"""
|
||||||
# Generated by NetworkManager
|
# Generated by NetworkManager
|
||||||
search pri
|
search pri
|
||||||
@ -156,7 +156,7 @@ nameserver 2404:6800:4004:80c::2
|
|||||||
nameserver 2404:6800:4004:80c::3
|
nameserver 2404:6800:4004:80c::3
|
||||||
nameserver 2404:6800:4004:80c::4
|
nameserver 2404:6800:4004:80c::4
|
||||||
""")
|
""")
|
||||||
ns = sshuttle.helpers.get_random_nameserver()
|
ns = sshuttle.helpers.resolvconf_random_nameserver()
|
||||||
assert ns in [
|
assert ns in [
|
||||||
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
||||||
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
||||||
@ -192,4 +192,5 @@ def test_family_ip_tuple():
|
|||||||
def test_family_to_string():
|
def test_family_to_string():
|
||||||
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
|
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
|
||||||
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
|
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
|
||||||
assert isinstance(sshuttle.helpers.family_to_string(socket.AF_UNIX), str)
|
expected = 'AddressFamily.AF_UNIX'
|
||||||
|
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected
|
||||||
|
@ -3,7 +3,7 @@ from socket import AF_INET, AF_INET6
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch, call
|
from mock import Mock, patch, call
|
||||||
from sshuttle.helpers import Fatal
|
from sshuttle.helpers import Fatal
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
@ -11,29 +11,19 @@ from sshuttle.methods import get_method
|
|||||||
def test_get_supported_features():
|
def test_get_supported_features():
|
||||||
method = get_method('nat')
|
method = get_method('nat')
|
||||||
features = method.get_supported_features()
|
features = method.get_supported_features()
|
||||||
assert features.ipv6
|
assert not features.ipv6
|
||||||
assert not features.udp
|
assert not features.udp
|
||||||
assert features.dns
|
assert features.dns
|
||||||
|
|
||||||
|
|
||||||
def test_get_tcp_dstip():
|
def test_get_tcp_dstip():
|
||||||
sock = Mock()
|
sock = Mock()
|
||||||
sock.family = AF_INET
|
|
||||||
sock.getsockopt.return_value = struct.pack(
|
sock.getsockopt.return_value = struct.pack(
|
||||||
'!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1)
|
'!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1)
|
||||||
method = get_method('nat')
|
method = get_method('nat')
|
||||||
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
||||||
assert sock.mock_calls == [call.getsockopt(0, 80, 16)]
|
assert sock.mock_calls == [call.getsockopt(0, 80, 16)]
|
||||||
|
|
||||||
sock = Mock()
|
|
||||||
sock.family = AF_INET6
|
|
||||||
sock.getsockopt.return_value = struct.pack(
|
|
||||||
'!HH4xBBBBBBBBBBBBBBBB', socket.ntohs(AF_INET6),
|
|
||||||
1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)
|
|
||||||
method = get_method('nft')
|
|
||||||
assert method.get_tcp_dstip(sock) == ('::1', 1024)
|
|
||||||
assert sock.mock_calls == [call.getsockopt(41, 80, 64)]
|
|
||||||
|
|
||||||
|
|
||||||
def test_recv_udp():
|
def test_recv_udp():
|
||||||
sock = Mock()
|
sock = Mock()
|
||||||
@ -81,57 +71,30 @@ def test_assert_features():
|
|||||||
|
|
||||||
def test_firewall_command():
|
def test_firewall_command():
|
||||||
method = get_method('nat')
|
method = get_method('nat')
|
||||||
assert not method.firewall_command("something")
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.nat.ipt')
|
@patch('sshuttle.methods.nat.ipt')
|
||||||
|
@patch('sshuttle.methods.nat.ipt_ttl')
|
||||||
@patch('sshuttle.methods.nat.ipt_chain_exists')
|
@patch('sshuttle.methods.nat.ipt_chain_exists')
|
||||||
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||||
mock_ipt_chain_exists.return_value = True
|
mock_ipt_chain_exists.return_value = True
|
||||||
method = get_method('nat')
|
method = get_method('nat')
|
||||||
assert method.name == 'nat'
|
assert method.name == 'nat'
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
|
AF_INET6,
|
||||||
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
||||||
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
||||||
|
True,
|
||||||
|
None)
|
||||||
|
assert str(excinfo.value) \
|
||||||
|
== 'Address family "AF_INET6" unsupported by nat method_name'
|
||||||
assert mock_ipt_chain_exists.mock_calls == []
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == []
|
assert mock_ipt_ttl.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 == []
|
assert mock_ipt.mock_calls == []
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
with pytest.raises(Exception) as excinfo:
|
||||||
@ -142,11 +105,10 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
True,
|
True,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert str(excinfo.value) == 'UDP not supported by nat method_name'
|
assert str(excinfo.value) == 'UDP not supported by nat method_name'
|
||||||
assert mock_ipt_chain_exists.mock_calls == []
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == []
|
assert mock_ipt.mock_calls == []
|
||||||
|
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
@ -156,12 +118,18 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET, 'nat', 'sshuttle-1025')
|
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == [
|
||||||
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||||
|
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
|
||||||
|
'--to-ports', '1025'),
|
||||||
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||||
|
'--dest', u'1.2.3.33/32', '-p', 'udp',
|
||||||
|
'--dport', '53', '--to-ports', '1027')
|
||||||
|
]
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||||
@ -171,45 +139,30 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
|
||||||
'--dest', u'1.2.3.33', '-p', 'udp',
|
|
||||||
'--dport', '53', '--to-ports', '1027'),
|
|
||||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080'),
|
'-m', 'addrtype', '--dst-type', 'LOCAL',
|
||||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
'!', '-p', 'udp'),
|
||||||
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
|
|
||||||
'--to-ports', '1025'),
|
|
||||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
'-m', 'addrtype', '--dst-type', 'LOCAL',
|
||||||
|
'-p', 'udp', '!', '--dport', '53'),
|
||||||
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET, False, None, None)
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET, 'nat', 'sshuttle-1025')
|
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j',
|
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j',
|
|
||||||
'sshuttle-1025'),
|
|
||||||
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
|
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt_ttl.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()
|
mock_ipt.reset_mock()
|
||||||
|
@ -2,9 +2,9 @@ import socket
|
|||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch, call, ANY
|
from mock import Mock, patch, call, ANY
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
from sshuttle.helpers import Fatal, get_env
|
from sshuttle.helpers import Fatal
|
||||||
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
|
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
|
||||||
|
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ def test_assert_features():
|
|||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
method = get_method('pf')
|
method = get_method('pf')
|
||||||
assert not method.firewall_command("something")
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
AF_INET, socket.IPPROTO_TCP,
|
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')
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
method = get_method('pf')
|
method = get_method('pf')
|
||||||
assert not method.firewall_command("something")
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
AF_INET, socket.IPPROTO_TCP,
|
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')
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
method = get_method('pf')
|
method = get_method('pf')
|
||||||
assert not method.firewall_command("something")
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
AF_INET, socket.IPPROTO_TCP,
|
AF_INET, socket.IPPROTO_TCP,
|
||||||
@ -186,9 +186,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
@ -227,9 +225,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
@ -242,9 +238,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
False,
|
False,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
@ -273,7 +267,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
mock_ioctl.reset_mock()
|
mock_ioctl.reset_mock()
|
||||||
mock_pfctl.reset_mock()
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET, False, None, None)
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-a sshuttle-1025 -F all'),
|
call('-a sshuttle-1025 -F all'),
|
||||||
@ -304,9 +298,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
|
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-s all'),
|
call('-s all'),
|
||||||
@ -324,8 +316,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
b'to <dns_servers> port 53 keep state\n'),
|
b'to <dns_servers> port 53 keep state\n'),
|
||||||
call('-e'),
|
call('-e'),
|
||||||
]
|
]
|
||||||
assert call(['kldload', 'pf'], env=get_env()) in \
|
assert call(['kldload', 'pf']) in mock_subprocess_call.mock_calls
|
||||||
mock_subprocess_call.mock_calls
|
|
||||||
mock_pf_get_dev.reset_mock()
|
mock_pf_get_dev.reset_mock()
|
||||||
mock_ioctl.reset_mock()
|
mock_ioctl.reset_mock()
|
||||||
mock_pfctl.reset_mock()
|
mock_pfctl.reset_mock()
|
||||||
@ -338,9 +329,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
@ -353,9 +342,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
False,
|
False,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
||||||
@ -382,8 +369,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
mock_ioctl.reset_mock()
|
mock_ioctl.reset_mock()
|
||||||
mock_pfctl.reset_mock()
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET, False, None, None)
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
method.restore_firewall(1024, AF_INET6, False, None, None)
|
method.restore_firewall(1024, AF_INET6, False, None)
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-a sshuttle-1025 -F all'),
|
call('-a sshuttle-1025 -F all'),
|
||||||
@ -413,13 +400,11 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
|
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||||
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||||
]
|
]
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-s Interfaces -i lo -v'),
|
call('-s Interfaces -i lo -v'),
|
||||||
@ -451,9 +436,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
@ -466,12 +449,10 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
False,
|
False,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||||
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||||
]
|
]
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-s Interfaces -i lo -v'),
|
call('-s Interfaces -i lo -v'),
|
||||||
@ -493,8 +474,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
mock_ioctl.reset_mock()
|
mock_ioctl.reset_mock()
|
||||||
mock_pfctl.reset_mock()
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET, False, None, None)
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
method.restore_firewall(1024, AF_INET6, False, None, None)
|
method.restore_firewall(1024, AF_INET6, False, None)
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-a sshuttle-1025 -F all'),
|
call('-a sshuttle-1025 -F all'),
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import socket
|
import socket
|
||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
from unittest.mock import Mock, patch, call
|
from mock import Mock, patch, call
|
||||||
|
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
|
|
||||||
def test_get_supported_features():
|
@patch("sshuttle.methods.tproxy.recvmsg")
|
||||||
|
def test_get_supported_features_recvmsg(mock_recvmsg):
|
||||||
method = get_method('tproxy')
|
method = get_method('tproxy')
|
||||||
features = method.get_supported_features()
|
features = method.get_supported_features()
|
||||||
assert features.ipv6
|
assert features.ipv6
|
||||||
@ -14,6 +15,15 @@ def test_get_supported_features():
|
|||||||
assert features.dns
|
assert features.dns
|
||||||
|
|
||||||
|
|
||||||
|
@patch("sshuttle.methods.tproxy.recvmsg", None)
|
||||||
|
def test_get_supported_features_norecvmsg():
|
||||||
|
method = get_method('tproxy')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
assert features.ipv6
|
||||||
|
assert not features.udp
|
||||||
|
assert not features.dns
|
||||||
|
|
||||||
|
|
||||||
def test_get_tcp_dstip():
|
def test_get_tcp_dstip():
|
||||||
sock = Mock()
|
sock = Mock()
|
||||||
sock.getsockname.return_value = ('127.0.0.1', 1024)
|
sock.getsockname.return_value = ('127.0.0.1', 1024)
|
||||||
@ -78,12 +88,13 @@ def test_assert_features():
|
|||||||
|
|
||||||
def test_firewall_command():
|
def test_firewall_command():
|
||||||
method = get_method('tproxy')
|
method = get_method('tproxy')
|
||||||
assert not method.firewall_command("something")
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.tproxy.ipt')
|
@patch('sshuttle.methods.tproxy.ipt')
|
||||||
|
@patch('sshuttle.methods.tproxy.ipt_ttl')
|
||||||
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
|
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
|
||||||
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||||
mock_ipt_chain_exists.return_value = True
|
mock_ipt_chain_exists.return_value = True
|
||||||
method = get_method('tproxy')
|
method = get_method('tproxy')
|
||||||
assert method.name == 'tproxy'
|
assert method.name == 'tproxy'
|
||||||
@ -97,14 +108,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
True,
|
True,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
|
call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-d-1024')
|
call(AF_INET6, 'mangle', 'sshuttle-d-1024')
|
||||||
]
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
|
||||||
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
|
||||||
@ -123,24 +133,20 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
||||||
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||||
'sshuttle-t-1024'),
|
'sshuttle-t-1024'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-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',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
|
||||||
'--set-mark', '0x01'),
|
'--set-mark', '1'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
|
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
|
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
|
'--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',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
||||||
'--dest', u'2404:6800:4004:80c::101f/128',
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
'-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
|
'-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
|
||||||
@ -154,31 +160,31 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
'--dest', u'2404:6800:4004:80c::101f/128',
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
|
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
|
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
|
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x01', '--dest',
|
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
u'2404:6800:4004:80c::/64',
|
|
||||||
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
|
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
|
||||||
'--on-port', '1024'),
|
'--on-port', '1024'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
|
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
|
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x01', '--dest',
|
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
u'2404:6800:4004:80c::/64',
|
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
|
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
|
||||||
'--on-port', '1024')
|
'--on-port', '1024')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET6, True, None, None)
|
method.restore_firewall(1025, AF_INET6, True, None)
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-m-1025'),
|
call(AF_INET6, 'mangle', 'sshuttle-m-1025'),
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-t-1025'),
|
call(AF_INET6, 'mangle', 'sshuttle-t-1025'),
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-d-1025')
|
call(AF_INET6, 'mangle', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'),
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
@ -190,6 +196,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
# IPV4
|
# IPV4
|
||||||
@ -201,14 +208,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None,
|
None)
|
||||||
None,
|
|
||||||
'0x01')
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
||||||
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
@ -227,23 +233,19 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||||
'sshuttle-t-1025'),
|
'sshuttle-t-1025'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-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',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
|
||||||
'-j', 'MARK', '--set-mark', '0x01'),
|
'-j', 'MARK', '--set-mark', '1'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
|
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
|
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
|
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
|
||||||
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
|
'--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',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
||||||
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
|
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
|
||||||
'--dport', '80:80'),
|
'--dport', '80:80'),
|
||||||
@ -257,27 +259,29 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
|
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
|
||||||
'--dport', '80:80'),
|
'--dport', '80:80'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
'--set-mark', '0x01', '--dest', u'1.2.3.0/24',
|
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'tcp', '-p', 'tcp'),
|
'-m', 'tcp', '-p', 'tcp'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24',
|
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
|
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
'--set-mark', '0x01', '--dest', u'1.2.3.0/24',
|
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'udp', '-p', 'udp'),
|
'-m', 'udp', '-p', 'udp'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24',
|
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
|
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET, True, None, None)
|
method.restore_firewall(1025, AF_INET, True, None)
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
||||||
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
@ -289,4 +293,5 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
|||||||
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
|
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import socket
|
import socket
|
||||||
from argparse import ArgumentTypeError as Fatal
|
from argparse import ArgumentTypeError as Fatal
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -14,6 +13,7 @@ _ip4_reprs = {
|
|||||||
'3098282570': '184.172.10.74',
|
'3098282570': '184.172.10.74',
|
||||||
'0xb8.0xac.0x0a.0x4a': '184.172.10.74',
|
'0xb8.0xac.0x0a.0x4a': '184.172.10.74',
|
||||||
'0270.0254.0012.0112': '184.172.10.74',
|
'0270.0254.0012.0112': '184.172.10.74',
|
||||||
|
'localhost': '127.0.0.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
_ip4_swidths = (1, 8, 22, 27, 32)
|
_ip4_swidths = (1, 8, 22, 27, 32)
|
||||||
@ -28,27 +28,10 @@ _ip6_reprs = {
|
|||||||
_ip6_swidths = (48, 64, 96, 115, 128)
|
_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():
|
def test_parse_subnetport_ip4():
|
||||||
for ip_repr, ip in _ip4_reprs.items():
|
for ip_repr, ip in _ip4_reprs.items():
|
||||||
assert sshuttle.options.parse_subnetport(ip_repr) \
|
assert sshuttle.options.parse_subnetport(ip_repr) \
|
||||||
== [(socket.AF_INET, ip, 32, 0, 0)]
|
== (socket.AF_INET, ip, 32, 0, 0)
|
||||||
with pytest.raises(Fatal) as excinfo:
|
with pytest.raises(Fatal) as excinfo:
|
||||||
sshuttle.options.parse_subnetport('10.256.0.0')
|
sshuttle.options.parse_subnetport('10.256.0.0')
|
||||||
assert str(excinfo.value) == 'Unable to resolve address: 10.256.0.0'
|
assert str(excinfo.value) == 'Unable to resolve address: 10.256.0.0'
|
||||||
@ -59,35 +42,34 @@ def test_parse_subnetport_ip4_with_mask():
|
|||||||
for swidth in _ip4_swidths:
|
for swidth in _ip4_swidths:
|
||||||
assert sshuttle.options.parse_subnetport(
|
assert sshuttle.options.parse_subnetport(
|
||||||
'/'.join((ip_repr, str(swidth)))
|
'/'.join((ip_repr, str(swidth)))
|
||||||
) == [(socket.AF_INET, ip, swidth, 0, 0)]
|
) == (socket.AF_INET, ip, swidth, 0, 0)
|
||||||
assert sshuttle.options.parse_subnetport('0/0') \
|
assert sshuttle.options.parse_subnetport('0/0') \
|
||||||
== [(socket.AF_INET, '0.0.0.0', 0, 0, 0)]
|
== (socket.AF_INET, '0.0.0.0', 0, 0, 0)
|
||||||
with pytest.raises(Fatal) as excinfo:
|
with pytest.raises(Fatal) as excinfo:
|
||||||
sshuttle.options.parse_subnetport('10.0.0.0/33')
|
sshuttle.options.parse_subnetport('10.0.0.0/33')
|
||||||
assert str(excinfo.value) \
|
assert str(excinfo.value) == 'width 33 is not between 0 and 32'
|
||||||
== 'Slash in CIDR notation (/33) is not between 0 and 32'
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subnetport_ip4_with_port():
|
def test_parse_subnetport_ip4_with_port():
|
||||||
for ip_repr, ip in _ip4_reprs.items():
|
for ip_repr, ip in _ip4_reprs.items():
|
||||||
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \
|
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \
|
||||||
== [(socket.AF_INET, ip, 32, 80, 80)]
|
== (socket.AF_INET, ip, 32, 80, 80)
|
||||||
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\
|
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\
|
||||||
== [(socket.AF_INET, ip, 32, 80, 90)]
|
== (socket.AF_INET, ip, 32, 80, 90)
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subnetport_ip4_with_mask_and_port():
|
def test_parse_subnetport_ip4_with_mask_and_port():
|
||||||
for ip_repr, ip in _ip4_reprs.items():
|
for ip_repr, ip in _ip4_reprs.items():
|
||||||
assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \
|
assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \
|
||||||
== [(socket.AF_INET, ip, 32, 80, 80)]
|
== (socket.AF_INET, ip, 32, 80, 80)
|
||||||
assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \
|
assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \
|
||||||
== [(socket.AF_INET, ip, 16, 80, 90)]
|
== (socket.AF_INET, ip, 16, 80, 90)
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subnetport_ip6():
|
def test_parse_subnetport_ip6():
|
||||||
for ip_repr, ip in _ip6_reprs.items():
|
for ip_repr, ip in _ip6_reprs.items():
|
||||||
assert sshuttle.options.parse_subnetport(ip_repr) \
|
assert sshuttle.options.parse_subnetport(ip_repr) \
|
||||||
== [(socket.AF_INET6, ip, 128, 0, 0)]
|
== (socket.AF_INET6, ip, 128, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subnetport_ip6_with_mask():
|
def test_parse_subnetport_ip6_with_mask():
|
||||||
@ -95,114 +77,25 @@ def test_parse_subnetport_ip6_with_mask():
|
|||||||
for swidth in _ip4_swidths + _ip6_swidths:
|
for swidth in _ip4_swidths + _ip6_swidths:
|
||||||
assert sshuttle.options.parse_subnetport(
|
assert sshuttle.options.parse_subnetport(
|
||||||
'/'.join((ip_repr, str(swidth)))
|
'/'.join((ip_repr, str(swidth)))
|
||||||
) == [(socket.AF_INET6, ip, swidth, 0, 0)]
|
) == (socket.AF_INET6, ip, swidth, 0, 0)
|
||||||
assert sshuttle.options.parse_subnetport('::/0') \
|
assert sshuttle.options.parse_subnetport('::/0') \
|
||||||
== [(socket.AF_INET6, '::', 0, 0, 0)]
|
== (socket.AF_INET6, '::', 0, 0, 0)
|
||||||
with pytest.raises(Fatal) as excinfo:
|
with pytest.raises(Fatal) as excinfo:
|
||||||
sshuttle.options.parse_subnetport('fc00::/129')
|
sshuttle.options.parse_subnetport('fc00::/129')
|
||||||
assert str(excinfo.value) \
|
assert str(excinfo.value) == 'width 129 is not between 0 and 128'
|
||||||
== 'Slash in CIDR notation (/129) is not between 0 and 128'
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subnetport_ip6_with_port():
|
def test_parse_subnetport_ip6_with_port():
|
||||||
for ip_repr, ip in _ip6_reprs.items():
|
for ip_repr, ip in _ip6_reprs.items():
|
||||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \
|
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \
|
||||||
== [(socket.AF_INET6, ip, 128, 80, 80)]
|
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \
|
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \
|
||||||
== [(socket.AF_INET6, ip, 128, 80, 90)]
|
== (socket.AF_INET6, ip, 128, 80, 90)
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subnetport_ip6_with_mask_and_port():
|
def test_parse_subnetport_ip6_with_mask_and_port():
|
||||||
for ip_repr, ip in _ip6_reprs.items():
|
for ip_repr, ip in _ip6_reprs.items():
|
||||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \
|
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \
|
||||||
== [(socket.AF_INET6, ip, 128, 80, 80)]
|
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
|
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
|
||||||
== [(socket.AF_INET6, ip, 16, 80, 90)]
|
== (socket.AF_INET6, ip, 16, 80, 90)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_arg_line_to_args_skips_comments():
|
|
||||||
parser = sshuttle.options.MyArgumentParser()
|
|
||||||
assert parser.convert_arg_line_to_args("# whatever something") == []
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import socket
|
import socket
|
||||||
|
|
||||||
from unittest.mock import Mock, patch, call
|
from mock import Mock, patch, call
|
||||||
|
|
||||||
import sshuttle.sdnotify
|
import sshuttle.sdnotify
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import io
|
import io
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from unittest.mock import patch, Mock
|
from mock import patch, Mock
|
||||||
|
|
||||||
import sshuttle.server
|
import sshuttle.server
|
||||||
|
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
from sshuttle.ssh import parse_hostport
|
|
||||||
|
|
||||||
|
|
||||||
def test_host_only():
|
|
||||||
assert parse_hostport("host") == (None, None, None, "host")
|
|
||||||
assert parse_hostport("1.2.3.4") == (None, None, None, "1.2.3.4")
|
|
||||||
assert parse_hostport("2001::1") == (None, None, None, "2001::1")
|
|
||||||
assert parse_hostport("[2001::1]") == (None, None, None, "2001::1")
|
|
||||||
|
|
||||||
|
|
||||||
def test_host_and_port():
|
|
||||||
assert parse_hostport("host:22") == (None, None, 22, "host")
|
|
||||||
assert parse_hostport("1.2.3.4:22") == (None, None, 22, "1.2.3.4")
|
|
||||||
assert parse_hostport("[2001::1]:22") == (None, None, 22, "2001::1")
|
|
||||||
|
|
||||||
|
|
||||||
def test_username_and_host():
|
|
||||||
assert parse_hostport("user@host") == ("user", None, None, "host")
|
|
||||||
assert parse_hostport("user:@host") == ("user", None, None, "host")
|
|
||||||
assert parse_hostport("user:pass@host") == ("user", "pass", None, "host")
|
|
12
tox.ini
12
tox.ini
@ -1,16 +1,16 @@
|
|||||||
[tox]
|
[tox]
|
||||||
downloadcache = {toxworkdir}/cache/
|
downloadcache = {toxworkdir}/cache/
|
||||||
envlist =
|
envlist =
|
||||||
|
py35,
|
||||||
|
py36,
|
||||||
|
py37,
|
||||||
py38,
|
py38,
|
||||||
py39,
|
|
||||||
py310,
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
basepython =
|
basepython =
|
||||||
py39: python3.9
|
py36: python3.6
|
||||||
py310: python3.10
|
py37: python3.7
|
||||||
py311: python3.11
|
py38: python3.8
|
||||||
py312: python3.12
|
|
||||||
commands =
|
commands =
|
||||||
pip install -e .
|
pip install -e .
|
||||||
# actual flake8 test
|
# actual flake8 test
|
||||||
|
Loading…
x
Reference in New Issue
Block a user