mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 16:50:34 +02:00
Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
5ce4e8c409 | |||
934fac9d6c | |||
e2624f533f | |||
375810a9a8 | |||
5942376090 | |||
ae3c022d1d | |||
63f94aa6ec | |||
7b662536ba | |||
cf867248c2 | |||
454262829c | |||
684417d363 | |||
0b7440e65c | |||
12138e2b8d | |||
7991e3d9a2 | |||
99c4abce81 | |||
a2d405a6a7 | |||
7fa927ef8c | |||
a1dd6859b0 | |||
8a123d9762 | |||
cbe3d1e402 | |||
340ccc705e | |||
1f5e6cea70 | |||
fd6b6bb71f | |||
5b08caaeb1 | |||
40f6c1d4f2 | |||
c09e2985f2 | |||
7725f93d94 | |||
75faa9b9e8 | |||
d910b64be7 | |||
3f0f88eb09 | |||
f7f9a4dbc6 | |||
bf294643e2 | |||
693ee40c48 | |||
a0d94367f6 | |||
ac4313deca | |||
9bcedf1904 | |||
62da70510e | |||
d08f78a2d9 | |||
85dc3199a3 | |||
6f12698209 | |||
4b6f7c6a65 | |||
a3396a443d | |||
339b5221bc | |||
1084c0f245 | |||
cda60a5233 | |||
b346e976eb | |||
7c2b3cd30e | |||
012fbcb587 | |||
4a1fe0fefe | |||
6abda35fce | |||
2f3171670c | |||
304aaa5e46 | |||
f05d6531f2 | |||
e6074ed52d | |||
ac36a8a20e | |||
09c3324978 | |||
81532b29a9 | |||
eaf55ed296 | |||
d1dbed04a0 | |||
dc2287ccf8 | |||
6272a0212c | |||
8364fd96e8 | |||
8da94c39ea | |||
60ee5b910b | |||
b9e7a80715 | |||
bac2a6b0c7 | |||
dff6950c4c | |||
df9625bbfd | |||
554b8e3ae5 | |||
b826ae6b91 | |||
51287dc4db | |||
ace8642950 | |||
c4255a23f0 | |||
6b8e402367 | |||
7a92183f59 | |||
81a598a4cc | |||
89a94ff150 | |||
72060abbef | |||
de8a19ce69 | |||
32fceefa76 | |||
b0799f8752 | |||
cd2d69ac08 | |||
1885974f52 | |||
dadfba488b | |||
8fa15c3ca8 | |||
e19fc01324 | |||
371258991f | |||
db9ec36fac | |||
d4d0fa945d | |||
4a84ad3be6 | |||
900acc3ac7 | |||
49f46cd528 | |||
7b8f140870 | |||
9c5517fd25 | |||
3f34e27a2c | |||
2f88fc93cf | |||
0c4c061123 | |||
482e0cbd00 | |||
7da3b024dd | |||
b09cc4595b | |||
c01794f232 | |||
338486930f | |||
bd2f960743 | |||
2c74476124 | |||
5a64c81b5b | |||
2408563f3b | |||
834ac02a5d | |||
bd3164db22 | |||
037ee9025e | |||
bf2db72393 | |||
f10535edf4 | |||
fd63611b5a | |||
9c3107bed7 | |||
fdcc840b7b | |||
348f0eb653 | |||
6cdae8c3e5 | |||
bdf2797b74 | |||
cc38cc2def | |||
4ccf528664 | |||
83c136d6e6 | |||
efbc4d066f | |||
a0f466a07c | |||
d660d8159b | |||
8d5e23477e | |||
a91e0c0470 | |||
c0938bc9a5 | |||
975d208d60 | |||
39a7b1b47f | |||
4ba7612d90 | |||
ea0559eaea | |||
6bd3bd738a | |||
116b1e22b1 | |||
694a9c8a5b | |||
264e4d94b8 | |||
afbdf8b606 | |||
9a4df1fdcf | |||
3d875b8ca8 | |||
313ada3ff7 | |||
934618b603 | |||
87bd34e094 | |||
83debdfb21 |
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@ -1,6 +1,7 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
enable-beta-ecosystems: true
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: pip
|
- package-ecosystem: uv
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
27
.github/workflows/pythonpackage.yml
vendored
27
.github/workflows/pythonpackage.yml
vendored
@ -8,8 +8,7 @@ on:
|
|||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
workflow_dispatch:
|
workflow_dispatch: {}
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -17,21 +16,23 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||||
|
poetry-version: ["main"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install uv
|
||||||
run: |
|
uses: astral-sh/setup-uv@v6
|
||||||
python -m pip install --upgrade pip
|
with:
|
||||||
pip install -r requirements-tests.txt
|
version: "0.4.30"
|
||||||
|
enable-cache: true
|
||||||
|
cache-dependency-glob: "uv.lock"
|
||||||
|
- name: Install the project
|
||||||
|
run: uv sync --all-extras --dev
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: |
|
run: uv run flake8 sshuttle tests --count --show-source --statistics
|
||||||
flake8 sshuttle tests --count --show-source --statistics
|
- name: Run the automated tests
|
||||||
- name: Test with pytest
|
run: uv run pytest -v
|
||||||
run: |
|
|
||||||
PYTHONPATH=$PWD pytest
|
|
||||||
|
66
.github/workflows/release-please.yml
vendored
Normal file
66
.github/workflows/release-please.yml
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
name: release-please
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
release-please:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
outputs:
|
||||||
|
release_created: ${{ steps.release.outputs.release_created }}
|
||||||
|
tag_name: ${{ steps.release.outputs.tag_name }}
|
||||||
|
steps:
|
||||||
|
- uses: googleapis/release-please-action@v4
|
||||||
|
id: release
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
|
||||||
|
release-type: python
|
||||||
|
|
||||||
|
build-pypi:
|
||||||
|
name: Build for pypi
|
||||||
|
needs: [release-please]
|
||||||
|
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
version: "0.4.30"
|
||||||
|
enable-cache: true
|
||||||
|
cache-dependency-glob: "uv.lock"
|
||||||
|
- name: Build project
|
||||||
|
run: uv build
|
||||||
|
- name: Store the distribution packages
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
upload-pypi:
|
||||||
|
name: Upload to pypi
|
||||||
|
needs: [build-pypi]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/sshuttle
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Publish package distributions to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
@ -3,13 +3,11 @@ version: 2
|
|||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-20.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.9"
|
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:
|
sphinx:
|
||||||
configuration: docs/conf.py
|
configuration: docs/conf.py
|
||||||
|
|
||||||
python:
|
|
||||||
install:
|
|
||||||
- requirements: requirements.txt
|
|
||||||
- method: setuptools
|
|
||||||
path: .
|
|
||||||
|
54
CHANGELOG.md
Normal file
54
CHANGELOG.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.3.1](https://github.com/sshuttle/sshuttle/compare/v1.3.0...v1.3.1) (2025-03-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add pycodestyle config ([5942376](https://github.com/sshuttle/sshuttle/commit/5942376090395d0a8dfe38fe012a519268199341))
|
||||||
|
* add python lint tools ([ae3c022](https://github.com/sshuttle/sshuttle/commit/ae3c022d1d67de92f1c4712d06eb8ae76c970624))
|
||||||
|
* correct bad version number at runtime ([7b66253](https://github.com/sshuttle/sshuttle/commit/7b662536ba92d724ed8f86a32a21282fea66047c))
|
||||||
|
* Restore "nft" method ([375810a](https://github.com/sshuttle/sshuttle/commit/375810a9a8910a51db22c9fe4c0658c39b16c9e7))
|
||||||
|
|
||||||
|
## [1.3.0](https://github.com/sshuttle/sshuttle/compare/v1.2.0...v1.3.0) (2025-02-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* switch to a network namespace on Linux ([8a123d9](https://github.com/sshuttle/sshuttle/commit/8a123d9762b84f168a8ca8c75f73e590954e122d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* prevent UnicodeDecodeError parsing iptables rule with comments ([cbe3d1e](https://github.com/sshuttle/sshuttle/commit/cbe3d1e402cac9d3fbc818fe0cb8a87be2e94348))
|
||||||
|
* remove temp build hack ([1f5e6ce](https://github.com/sshuttle/sshuttle/commit/1f5e6cea703db33761fb1c3f999b9624cf3bc7ad))
|
||||||
|
* support ':' sign in password ([7fa927e](https://github.com/sshuttle/sshuttle/commit/7fa927ef8ceea6b1b2848ca433b8b3e3b63f0509))
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* replace nix-env with nix-shell ([340ccc7](https://github.com/sshuttle/sshuttle/commit/340ccc705ebd9499f14f799fcef0b5d2a8055fb4))
|
||||||
|
* update installation instructions ([a2d405a](https://github.com/sshuttle/sshuttle/commit/a2d405a6a7f9d1a301311a109f8411f2fe8deb37))
|
||||||
|
|
||||||
|
## [1.2.0](https://github.com/sshuttle/sshuttle/compare/v1.1.2...v1.2.0) (2025-02-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add release-please to build workflow ([d910b64](https://github.com/sshuttle/sshuttle/commit/d910b64be77fd7ef2a5f169b780bfda95e67318d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add support for Python 3.11 and Python 3.11 ([a3396a4](https://github.com/sshuttle/sshuttle/commit/a3396a443df14d3bafc3d25909d9221aa182b8fc))
|
||||||
|
* bad file descriptor error in windows, fix pytest errors ([d4d0fa9](https://github.com/sshuttle/sshuttle/commit/d4d0fa945d50606360aa7c5f026a0f190b026c68))
|
||||||
|
* drop Python 3.8 support ([1084c0f](https://github.com/sshuttle/sshuttle/commit/1084c0f2458c1595b00963b3bd54bd667e4cfc9f))
|
||||||
|
* ensure poetry works for Python 3.9 ([693ee40](https://github.com/sshuttle/sshuttle/commit/693ee40c485c70f353326eb0e8f721f984850f5c))
|
||||||
|
* fix broken workflow_dispatch CI rule ([4b6f7c6](https://github.com/sshuttle/sshuttle/commit/4b6f7c6a656a752552295863092d3b8af0b42b31))
|
||||||
|
* Remove more references to legacy Python versions ([339b522](https://github.com/sshuttle/sshuttle/commit/339b5221bc33254329f79f2374f6114be6f30aed))
|
||||||
|
* replace requirements.txt files with poetry ([85dc319](https://github.com/sshuttle/sshuttle/commit/85dc3199a332f9f9f0e4c6037c883a8f88dc09ca))
|
||||||
|
* replace requirements.txt files with poetry (2) ([d08f78a](https://github.com/sshuttle/sshuttle/commit/d08f78a2d9777951d7e18f6eaebbcdd279d7683a))
|
||||||
|
* replace requirements.txt files with poetry (3) ([62da705](https://github.com/sshuttle/sshuttle/commit/62da70510e8a1f93e8b38870fdebdbace965cd8e))
|
||||||
|
* replace requirements.txt files with poetry (4) ([9bcedf1](https://github.com/sshuttle/sshuttle/commit/9bcedf19049e5b3a8ae26818299cc518ec03a926))
|
||||||
|
* update nix flake to fix problems ([cda60a5](https://github.com/sshuttle/sshuttle/commit/cda60a52331c7102cff892b9b77c8321e276680a))
|
||||||
|
* use Python >= 3.10 for docs ([bf29464](https://github.com/sshuttle/sshuttle/commit/bf294643e283cef9fb285d44e307e958686caf46))
|
81
README.rst
81
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, or MacOS.
|
- Your client machine (or router) is Linux, FreeBSD, MacOS or Windows.
|
||||||
|
|
||||||
- You have access to a remote network via ssh.
|
- You have access to a remote network via ssh.
|
||||||
|
|
||||||
@ -30,84 +30,9 @@ common case:
|
|||||||
Obtaining sshuttle
|
Obtaining sshuttle
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
- Ubuntu 16.04 or later::
|
Please see the documentation_.
|
||||||
|
|
||||||
apt-get install sshuttle
|
|
||||||
|
|
||||||
- Debian stretch or later::
|
|
||||||
|
|
||||||
apt-get install sshuttle
|
|
||||||
|
|
||||||
- Arch Linux::
|
|
||||||
|
|
||||||
pacman -S sshuttle
|
|
||||||
|
|
||||||
- Fedora::
|
|
||||||
|
|
||||||
dnf install sshuttle
|
|
||||||
|
|
||||||
- openSUSE::
|
|
||||||
|
|
||||||
zypper in sshuttle
|
|
||||||
|
|
||||||
- Gentoo::
|
|
||||||
|
|
||||||
emerge -av net-proxy/sshuttle
|
|
||||||
|
|
||||||
- NixOS::
|
|
||||||
|
|
||||||
nix-env -iA nixos.sshuttle
|
|
||||||
|
|
||||||
- From PyPI::
|
|
||||||
|
|
||||||
sudo pip install sshuttle
|
|
||||||
|
|
||||||
- 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 py39-sshuttle
|
|
||||||
|
|
||||||
- OpenBSD::
|
|
||||||
|
|
||||||
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::
|
|
||||||
|
|
||||||
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
|
||||||
-------------
|
-------------
|
||||||
|
@ -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.version # NOQA
|
import sshuttle # 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.version
|
release = sshuttle.__version__
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '.'.join(release.split('.')[:2])
|
version = '.'.join(release.split('.')[:2])
|
||||||
|
|
||||||
|
@ -1,23 +1,84 @@
|
|||||||
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::
|
||||||
|
|
||||||
pip install sshuttle
|
sudo pip install sshuttle
|
||||||
|
|
||||||
- Debian 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
|
||||||
./setup.py install
|
sudo ./setup.py install
|
||||||
|
|
||||||
|
- FreeBSD::
|
||||||
|
|
||||||
Optionally after installation
|
# ports
|
||||||
-----------------------------
|
cd /usr/ports/net/py-sshuttle && make install clean
|
||||||
|
# pkg
|
||||||
|
pkg install py39-sshuttle
|
||||||
|
|
||||||
- Install sudoers configuration. For details, see the "Sudoers File" section in :doc:`usage`
|
- OpenBSD::
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -181,6 +181,18 @@ 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
|
||||||
|
@ -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.8 or greater.
|
- Python 3.9 or greater.
|
||||||
|
|
||||||
|
|
||||||
Linux with NAT method
|
Linux with NAT method
|
||||||
@ -65,14 +65,13 @@ Requires:
|
|||||||
Windows
|
Windows
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
Not officially supported, however can be made to work with Vagrant. Requires
|
Experimental built-in support available. See :doc:`windows` for more information.
|
||||||
cmd.exe with Administrator access. See :doc:`windows` for more information.
|
|
||||||
|
|
||||||
|
|
||||||
Server side Requirements
|
Server side Requirements
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
- Python 3.8 or greater.
|
- Python 3.9 or greater.
|
||||||
|
|
||||||
|
|
||||||
Additional Suggested Software
|
Additional Suggested Software
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
Microsoft Windows
|
Microsoft Windows
|
||||||
=================
|
=================
|
||||||
Currently there is no built in support for running sshuttle directly on
|
|
||||||
Microsoft Windows.
|
Experimental native support::
|
||||||
|
|
||||||
|
Experimental built-in support for Windows is available through `windivert` method.
|
||||||
|
You have to install https://pypi.org/project/pydivert package. You need Administrator privileges to use windivert method
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- sshuttle should be executed from admin shell (Automatic firewall process admin elevation is not available)
|
||||||
|
- TCP/IPv4 supported (IPv6/UDP/DNS are not available)
|
||||||
|
|
||||||
|
Use Linux VM on Windows::
|
||||||
|
|
||||||
What we can really do is to create a Linux VM with Vagrant (or simply
|
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
|
||||||
|
152
flake.lock
generated
152
flake.lock
generated
@ -5,11 +5,11 @@
|
|||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1705309234,
|
"lastModified": 1731533236,
|
||||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -18,82 +18,65 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1694529238,
|
|
||||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nix-github-actions": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"poetry2nix",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1698974481,
|
|
||||||
"narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nix-github-actions",
|
|
||||||
"rev": "4bb5e752616262457bc7ca5882192a564c0472d2",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nix-github-actions",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708161998,
|
"lastModified": 1740743217,
|
||||||
"narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=",
|
"narHash": "sha256-brsCRzLqimpyhORma84c3W2xPbIidZlIc3JGIuQVSNI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "84d981bae8b5e783b3b548de505b22880559515f",
|
"rev": "b27ba4eb322d9d2bf2dc9ada9fd59442f50c8d7c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-23.11",
|
"ref": "nixos-24.11",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"poetry2nix": {
|
"pyproject-build-systems": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nix-github-actions": "nix-github-actions",
|
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
],
|
],
|
||||||
"systems": "systems_3",
|
"pyproject-nix": [
|
||||||
"treefmt-nix": "treefmt-nix"
|
"pyproject-nix"
|
||||||
|
],
|
||||||
|
"uv2nix": [
|
||||||
|
"uv2nix"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708175019,
|
"lastModified": 1740362541,
|
||||||
"narHash": "sha256-B7wY2pNrLc3X9uYRo1LUmVzI6oH6fX8oi+96GdUpayU=",
|
"narHash": "sha256-S8Mno07MspggOv/xIz5g8hB2b/C5HPiX8E+rXzKY+5U=",
|
||||||
"owner": "nix-community",
|
"owner": "pyproject-nix",
|
||||||
"repo": "poetry2nix",
|
"repo": "build-system-pkgs",
|
||||||
"rev": "403d923ea8e2e6cedce3a0f04a9394c4244cb806",
|
"rev": "e151741c848ba92331af91f4e47640a1fb82be19",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "pyproject-nix",
|
||||||
"repo": "poetry2nix",
|
"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"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -101,7 +84,9 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"poetry2nix": "poetry2nix"
|
"pyproject-build-systems": "pyproject-build-systems",
|
||||||
|
"pyproject-nix": "pyproject-nix",
|
||||||
|
"uv2nix": "uv2nix"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
@ -119,53 +104,26 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems_2": {
|
"uv2nix": {
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_3": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "systems",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"treefmt-nix": {
|
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"poetry2nix",
|
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"pyproject-nix": [
|
||||||
|
"pyproject-nix"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1699786194,
|
"lastModified": 1740497536,
|
||||||
"narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=",
|
"narHash": "sha256-K+8wsVooqhaqyxuvew3+62mgOfRLJ7whv7woqPU3Ypo=",
|
||||||
"owner": "numtide",
|
"owner": "pyproject-nix",
|
||||||
"repo": "treefmt-nix",
|
"repo": "uv2nix",
|
||||||
"rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1",
|
"rev": "d01fd3a141755ad5d5b93dd9fcbd76d6401f5bac",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "numtide",
|
"owner": "pyproject-nix",
|
||||||
"repo": "treefmt-nix",
|
"repo": "uv2nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
141
flake.nix
141
flake.nix
@ -1,42 +1,117 @@
|
|||||||
{
|
{
|
||||||
description =
|
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.";
|
||||||
"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";
|
inputs = {
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
inputs.poetry2nix = {
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
url = "github:nix-community/poetry2nix";
|
pyproject-nix = {
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
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, poetry2nix }:
|
outputs =
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
pyproject-nix,
|
||||||
|
uv2nix,
|
||||||
|
pyproject-build-systems,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
let
|
let
|
||||||
p2n = import poetry2nix { inherit pkgs; };
|
inherit (nixpkgs) lib;
|
||||||
overrides = p2n.defaultPoetryOverrides.extend (self: super: {
|
|
||||||
nh3 = super.nh3.override { preferWheel = true; };
|
|
||||||
bump2version = super.bump2version.overridePythonAttrs (old: {
|
|
||||||
buildInputs = (old.buildInputs or [ ]) ++ [ super.setuptools ];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
poetry_env = p2n.mkPoetryEnv {
|
|
||||||
python = pkgs.python3;
|
|
||||||
projectDir = self;
|
|
||||||
inherit overrides;
|
|
||||||
};
|
|
||||||
poetry_app = p2n.mkPoetryApplication {
|
|
||||||
python = pkgs.python3;
|
|
||||||
projectDir = self;
|
|
||||||
inherit overrides;
|
|
||||||
};
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in {
|
|
||||||
packages = {
|
python = pkgs.python312;
|
||||||
sshuttle = poetry_app;
|
|
||||||
default = self.packages.${system}.sshuttle;
|
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
|
||||||
|
|
||||||
|
# Create package overlay from workspace.
|
||||||
|
overlay = workspace.mkPyprojectOverlay {
|
||||||
|
sourcePreference = "sdist";
|
||||||
};
|
};
|
||||||
devShells.default =
|
|
||||||
pkgs.mkShell { packages = [ pkgs.poetry poetry_env ]; };
|
# 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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
848
poetry.lock
generated
848
poetry.lock
generated
@ -1,848 +0,0 @@
|
|||||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bump2version"
|
|
||||||
version = "1.0.1"
|
|
||||||
description = "Version-bump your software with a single command!"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
files = [
|
|
||||||
{file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"},
|
|
||||||
{file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2024.2.2"
|
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
|
|
||||||
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cffi"
|
|
||||||
version = "1.16.0"
|
|
||||||
description = "Foreign Function Interface for Python calling C code."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
|
|
||||||
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pycparser = "*"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "charset-normalizer"
|
|
||||||
version = "3.3.2"
|
|
||||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7.0"
|
|
||||||
files = [
|
|
||||||
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
|
|
||||||
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
|
|
||||||
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
description = "Cross-platform colored terminal text."
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
|
||||||
files = [
|
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "coverage"
|
|
||||||
version = "7.4.1"
|
|
||||||
description = "Code coverage measurement for Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"},
|
|
||||||
{file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"},
|
|
||||||
{file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"},
|
|
||||||
{file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"},
|
|
||||||
{file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"},
|
|
||||||
{file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"},
|
|
||||||
{file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"},
|
|
||||||
{file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
toml = ["tomli"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cryptography"
|
|
||||||
version = "42.0.3"
|
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"},
|
|
||||||
{file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"},
|
|
||||||
{file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"},
|
|
||||||
{file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"},
|
|
||||||
{file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"},
|
|
||||||
{file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"},
|
|
||||||
{file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"},
|
|
||||||
{file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"},
|
|
||||||
{file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"},
|
|
||||||
{file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"},
|
|
||||||
{file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"},
|
|
||||||
{file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
|
||||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
|
||||||
nox = ["nox"]
|
|
||||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
|
||||||
sdist = ["build"]
|
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
|
||||||
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
|
||||||
test-randomorder = ["pytest-randomly"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "docutils"
|
|
||||||
version = "0.20.1"
|
|
||||||
description = "Docutils -- Python Documentation Utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"},
|
|
||||||
{file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "exceptiongroup"
|
|
||||||
version = "1.2.0"
|
|
||||||
description = "Backport of PEP 654 (exception groups)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
|
|
||||||
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
test = ["pytest (>=6)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flake8"
|
|
||||||
version = "7.0.0"
|
|
||||||
description = "the modular source code checker: pep8 pyflakes and co"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8.1"
|
|
||||||
files = [
|
|
||||||
{file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"},
|
|
||||||
{file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
mccabe = ">=0.7.0,<0.8.0"
|
|
||||||
pycodestyle = ">=2.11.0,<2.12.0"
|
|
||||||
pyflakes = ">=3.2.0,<3.3.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.6"
|
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
files = [
|
|
||||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
|
||||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "importlib-metadata"
|
|
||||||
version = "7.0.1"
|
|
||||||
description = "Read metadata from Python packages"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"},
|
|
||||||
{file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
zipp = ">=0.5"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
|
||||||
perf = ["ipython"]
|
|
||||||
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iniconfig"
|
|
||||||
version = "2.0.0"
|
|
||||||
description = "brain-dead simple config-ini parsing"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
|
||||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jaraco-classes"
|
|
||||||
version = "3.3.1"
|
|
||||||
description = "Utility functions for Python class constructs"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "jaraco.classes-3.3.1-py3-none-any.whl", hash = "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206"},
|
|
||||||
{file = "jaraco.classes-3.3.1.tar.gz", hash = "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
more-itertools = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
|
||||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jeepney"
|
|
||||||
version = "0.8.0"
|
|
||||||
description = "Low-level, pure Python DBus protocol wrapper."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"},
|
|
||||||
{file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
|
|
||||||
trio = ["async_generator", "trio"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "keyring"
|
|
||||||
version = "24.3.0"
|
|
||||||
description = "Store and access your passwords safely."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "keyring-24.3.0-py3-none-any.whl", hash = "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836"},
|
|
||||||
{file = "keyring-24.3.0.tar.gz", hash = "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
|
|
||||||
"jaraco.classes" = "*"
|
|
||||||
jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
|
|
||||||
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
|
|
||||||
SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
completion = ["shtab (>=1.1.0)"]
|
|
||||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
|
||||||
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markdown-it-py"
|
|
||||||
version = "3.0.0"
|
|
||||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
|
||||||
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
mdurl = ">=0.1,<1.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
|
||||||
code-style = ["pre-commit (>=3.0,<4.0)"]
|
|
||||||
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
|
|
||||||
linkify = ["linkify-it-py (>=1,<3)"]
|
|
||||||
plugins = ["mdit-py-plugins"]
|
|
||||||
profiling = ["gprof2dot"]
|
|
||||||
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
|
|
||||||
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mccabe"
|
|
||||||
version = "0.7.0"
|
|
||||||
description = "McCabe checker, plugin for flake8"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
|
||||||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mdurl"
|
|
||||||
version = "0.1.2"
|
|
||||||
description = "Markdown URL utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
|
||||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "more-itertools"
|
|
||||||
version = "10.2.0"
|
|
||||||
description = "More routines for operating on iterables, beyond itertools"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"},
|
|
||||||
{file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nh3"
|
|
||||||
version = "0.2.15"
|
|
||||||
description = "Python bindings to the ammonia HTML sanitization library."
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"},
|
|
||||||
{file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"},
|
|
||||||
{file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "23.2"
|
|
||||||
description = "Core utilities for Python packages"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
|
||||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkginfo"
|
|
||||||
version = "1.9.6"
|
|
||||||
description = "Query metadata from sdists / bdists / installed packages."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"},
|
|
||||||
{file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
testing = ["pytest", "pytest-cov"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pluggy"
|
|
||||||
version = "1.4.0"
|
|
||||||
description = "plugin and hook calling mechanisms for python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
|
|
||||||
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dev = ["pre-commit", "tox"]
|
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycodestyle"
|
|
||||||
version = "2.11.1"
|
|
||||||
description = "Python style guide checker"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
|
|
||||||
{file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycparser"
|
|
||||||
version = "2.21"
|
|
||||||
description = "C parser in Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
files = [
|
|
||||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
|
||||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyflakes"
|
|
||||||
version = "3.2.0"
|
|
||||||
description = "passive checker of Python programs"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
|
|
||||||
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygments"
|
|
||||||
version = "2.17.2"
|
|
||||||
description = "Pygments is a syntax highlighting package written in Python."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
|
|
||||||
{file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
plugins = ["importlib-metadata"]
|
|
||||||
windows-terminal = ["colorama (>=0.4.6)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytest"
|
|
||||||
version = "8.0.1"
|
|
||||||
description = "pytest: simple powerful testing with Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"},
|
|
||||||
{file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
|
||||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
|
||||||
iniconfig = "*"
|
|
||||||
packaging = "*"
|
|
||||||
pluggy = ">=1.3.0,<2.0"
|
|
||||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytest-cov"
|
|
||||||
version = "4.1.0"
|
|
||||||
description = "Pytest plugin for measuring coverage."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
|
|
||||||
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
|
||||||
pytest = ">=4.6"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pywin32-ctypes"
|
|
||||||
version = "0.2.2"
|
|
||||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
|
|
||||||
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "readme-renderer"
|
|
||||||
version = "42.0"
|
|
||||||
description = "readme_renderer is a library for rendering readme descriptions for Warehouse"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "readme_renderer-42.0-py3-none-any.whl", hash = "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d"},
|
|
||||||
{file = "readme_renderer-42.0.tar.gz", hash = "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
docutils = ">=0.13.1"
|
|
||||||
nh3 = ">=0.2.14"
|
|
||||||
Pygments = ">=2.5.1"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
md = ["cmarkgfm (>=0.8.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests"
|
|
||||||
version = "2.31.0"
|
|
||||||
description = "Python HTTP for Humans."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
|
||||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
certifi = ">=2017.4.17"
|
|
||||||
charset-normalizer = ">=2,<4"
|
|
||||||
idna = ">=2.5,<4"
|
|
||||||
urllib3 = ">=1.21.1,<3"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
|
||||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests-toolbelt"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "A utility belt for advanced users of python-requests"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
files = [
|
|
||||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
|
||||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
requests = ">=2.0.1,<3.0.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rfc3986"
|
|
||||||
version = "2.0.0"
|
|
||||||
description = "Validating URI References per RFC 3986"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"},
|
|
||||||
{file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
idna2008 = ["idna"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rich"
|
|
||||||
version = "13.7.0"
|
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7.0"
|
|
||||||
files = [
|
|
||||||
{file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
|
|
||||||
{file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
markdown-it-py = ">=2.2.0"
|
|
||||||
pygments = ">=2.13.0,<3.0.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "secretstorage"
|
|
||||||
version = "3.3.3"
|
|
||||||
description = "Python bindings to FreeDesktop.org Secret Service API"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"},
|
|
||||||
{file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
cryptography = ">=2.0"
|
|
||||||
jeepney = ">=0.6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tomli"
|
|
||||||
version = "2.0.1"
|
|
||||||
description = "A lil' TOML parser"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "twine"
|
|
||||||
version = "5.0.0"
|
|
||||||
description = "Collection of utilities for publishing packages on PyPI"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "twine-5.0.0-py3-none-any.whl", hash = "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0"},
|
|
||||||
{file = "twine-5.0.0.tar.gz", hash = "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
importlib-metadata = ">=3.6"
|
|
||||||
keyring = ">=15.1"
|
|
||||||
pkginfo = ">=1.8.1"
|
|
||||||
readme-renderer = ">=35.0"
|
|
||||||
requests = ">=2.20"
|
|
||||||
requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"
|
|
||||||
rfc3986 = ">=1.4.0"
|
|
||||||
rich = ">=12.0.0"
|
|
||||||
urllib3 = ">=1.26.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urllib3"
|
|
||||||
version = "2.2.1"
|
|
||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
|
|
||||||
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
|
||||||
h2 = ["h2 (>=4,<5)"]
|
|
||||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zipp"
|
|
||||||
version = "3.17.0"
|
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
|
|
||||||
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
|
||||||
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
lock-version = "2.0"
|
|
||||||
python-versions = "^3.10"
|
|
||||||
content-hash = "e089c5db68017b943468ee1860df31baf7ec88633ce8aaaa25e82df84ed59a4d"
|
|
@ -1,25 +1,57 @@
|
|||||||
[tool.poetry]
|
[project]
|
||||||
|
authors = [
|
||||||
|
{name = "Brian May", email = "brian@linuxpenguins.xyz"},
|
||||||
|
]
|
||||||
|
license = {text = "LGPL-2.1"}
|
||||||
|
requires-python = "<4.0,>=3.9"
|
||||||
|
dependencies = []
|
||||||
name = "sshuttle"
|
name = "sshuttle"
|
||||||
version = "1.1.2"
|
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."
|
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."
|
||||||
authors = ["Brian May <brian@linuxpenguins.xyz>"]
|
|
||||||
license = "LGPL-2.1"
|
|
||||||
readme = "README.rst"
|
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",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[project.scripts]
|
||||||
python = "^3.10"
|
sshuttle = "sshuttle.cmdline:main"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[dependency-groups]
|
||||||
pytest = "^8.0.1"
|
dev = [
|
||||||
pytest-cov = "^4.1.0"
|
"pytest<9.0.0,>=8.0.1",
|
||||||
flake8 = "^7.0.0"
|
"pytest-cov<7.0,>=4.1",
|
||||||
pyflakes = "^3.2.0"
|
"flake8<8.0.0,>=7.0.0",
|
||||||
bump2version = "^1.0.1"
|
"pyflakes<4.0.0,>=3.2.0",
|
||||||
twine = "^5.0.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]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["hatchling"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.hatch.build.targets.sdist]
|
||||||
sshuttle = "sshuttle.cmdline:main"
|
exclude = [
|
||||||
|
"/.jj"
|
||||||
|
]
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
-r requirements.txt
|
|
||||||
pytest==8.0.0
|
|
||||||
pytest-cov==4.1.0
|
|
||||||
flake8==7.0.0
|
|
||||||
bump2version==1.0.1
|
|
@ -1,2 +0,0 @@
|
|||||||
Sphinx==7.1.2
|
|
||||||
furo==2024.1.29
|
|
39
scripts/Containerfile
Normal file
39
scripts/Containerfile
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# https://hub.docker.com/r/linuxserver/openssh-server/
|
||||||
|
ARG BASE_IMAGE=docker.io/linuxserver/openssh-server:version-9.3_p2-r1
|
||||||
|
|
||||||
|
FROM ${BASE_IMAGE} as pyenv
|
||||||
|
|
||||||
|
# https://github.com/pyenv/pyenv/wiki#suggested-build-environment
|
||||||
|
RUN apk add --no-cache build-base git libffi-dev openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev
|
||||||
|
ENV PYENV_ROOT=/pyenv
|
||||||
|
RUN curl https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
|
||||||
|
RUN /pyenv/bin/pyenv install 3.10
|
||||||
|
RUN /pyenv/bin/pyenv install 3.11
|
||||||
|
RUN /pyenv/bin/pyenv install 3.12
|
||||||
|
RUN bash -xc 'rm -rf /pyenv/{.git,plugins} /pyenv/versions/*/lib/*/{test,config,config-*linux-gnu}' && \
|
||||||
|
find /pyenv -type d -name __pycache__ -exec rm -rf {} + && \
|
||||||
|
find /pyenv -type f -name '*.py[co]' -delete
|
||||||
|
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
|
RUN apk add --no-cache bash nginx iperf3
|
||||||
|
|
||||||
|
# pyenv setup
|
||||||
|
ENV PYENV_ROOT=/pyenv
|
||||||
|
ENV PATH=/pyenv/shims:/pyenv/bin:$PATH
|
||||||
|
COPY --from=pyenv /pyenv /pyenv
|
||||||
|
|
||||||
|
# OpenSSH Server variables
|
||||||
|
ENV PUID=1000
|
||||||
|
ENV PGID=1000
|
||||||
|
ENV PASSWORD_ACCESS=true
|
||||||
|
ENV USER_NAME=test
|
||||||
|
ENV USER_PASSWORD=test
|
||||||
|
ENV LOG_STDOUT=true
|
||||||
|
|
||||||
|
# suppress linuxserver.io logo printing, chnage sshd config
|
||||||
|
RUN sed -i '1 a exec &>/dev/null' /etc/s6-overlay/s6-rc.d/init-adduser/run
|
||||||
|
|
||||||
|
# https://www.linuxserver.io/blog/2019-09-14-customizing-our-containers
|
||||||
|
# To customize the container and start other components
|
||||||
|
COPY container.setup.sh /custom-cont-init.d/setup.sh
|
21
scripts/README.md
Normal file
21
scripts/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Container based test bed for sshuttle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test-bed up -d # start containers
|
||||||
|
|
||||||
|
exec-sshuttle <node-id> [--copy-id] [--server-py=2.7|3.10] [--client-py=2.7|3.10] [--sshuttle-bin=/path/to/sshuttle] [sshuttle-args...]
|
||||||
|
# --copy-id -> optionally do ssh-copy-id to make it passwordless for future runs
|
||||||
|
# --sshuttle-bin -> use another sshuttle binary instead of one from dev setup
|
||||||
|
# --server-py -> Python version to use in server. (manged by pyenv)
|
||||||
|
# --client-py -> Python version to use in client (manged by pyenv)
|
||||||
|
|
||||||
|
exec-sshuttle node-1 # start sshuttle to connect to node-1
|
||||||
|
|
||||||
|
exec-tool curl node-1 # curl to nginx instance running on node1 via IP that is only reachable via sshuttle
|
||||||
|
exec-tool iperf3 node-1 # measure throughput to node-1
|
||||||
|
|
||||||
|
run-benchmark node-1 --client-py=3.10
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
<https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_server_configuration#configuring-the-default-shell-for-openssh-in-windows>
|
34
scripts/compose.yml
Normal file
34
scripts/compose.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: sshuttle-testbed
|
||||||
|
|
||||||
|
services:
|
||||||
|
node-1:
|
||||||
|
image: ghcr.io/sshuttle/sshuttle-testbed
|
||||||
|
container_name: sshuttle-testbed-node-1
|
||||||
|
hostname: node-1
|
||||||
|
cap_add:
|
||||||
|
- "NET_ADMIN"
|
||||||
|
environment:
|
||||||
|
- ADD_IP_ADDRESSES=10.55.1.77/24
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
ipv6_address: 2001:0DB8::551
|
||||||
|
node-2:
|
||||||
|
image: ghcr.io/sshuttle/sshuttle-testbed
|
||||||
|
container_name: sshuttle-testbed-node-2
|
||||||
|
hostname: node-2
|
||||||
|
cap_add:
|
||||||
|
- "NET_ADMIN"
|
||||||
|
environment:
|
||||||
|
- ADD_IP_ADDRESSES=10.55.2.77/32
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
ipv6_address: 2001:0DB8::552
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
enable_ipv6: true
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 2001:0DB8::/112
|
||||||
|
# internal: true
|
65
scripts/container.setup.sh
Executable file
65
scripts/container.setup.sh
Executable file
@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/with-contenv bash
|
||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function with_set_x() {
|
||||||
|
set -x
|
||||||
|
"$@"
|
||||||
|
{
|
||||||
|
ec=$?
|
||||||
|
set +x
|
||||||
|
return $ec
|
||||||
|
} 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
echo "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log ">>> Setting up $(hostname) | id: $(id)\nIP:\n$(ip a)\nRoutes:\n$(ip r)\npyenv:\n$(pyenv versions)"
|
||||||
|
|
||||||
|
echo "
|
||||||
|
AcceptEnv PYENV_VERSION
|
||||||
|
" >> /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
iface="$(ip route | awk '/default/ { print $5 }')"
|
||||||
|
default_gw="$(ip route | awk '/default/ { print $3 }')"
|
||||||
|
for addr in ${ADD_IP_ADDRESSES//,/ }; do
|
||||||
|
log ">>> Adding $addr to interface $iface"
|
||||||
|
net_addr=$(ipcalc -n "$addr" | awk -F= '{print $2}')
|
||||||
|
with_set_x ip addr add "$addr" dev "$iface"
|
||||||
|
with_set_x ip route add "$net_addr" via "$default_gw" dev "$iface" # so that sshuttle -N can discover routes
|
||||||
|
done
|
||||||
|
|
||||||
|
log ">>> Starting iperf3 server"
|
||||||
|
iperf3 --server --port 5001 &
|
||||||
|
|
||||||
|
mkdir -p /www
|
||||||
|
echo "<h5>Hello from $(hostname)</h5>
|
||||||
|
<pre>
|
||||||
|
<u>ip address</u>
|
||||||
|
$(ip address)
|
||||||
|
<u>ip route</u>
|
||||||
|
$(ip route)
|
||||||
|
</pre>" >/www/index.html
|
||||||
|
echo "
|
||||||
|
daemon off;
|
||||||
|
worker_processes 1;
|
||||||
|
error_log /dev/stdout info;
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
server {
|
||||||
|
access_log /dev/stdout;
|
||||||
|
listen 8080 default_server;
|
||||||
|
listen [::]:8080 default_server;
|
||||||
|
root /www;
|
||||||
|
}
|
||||||
|
}" >/etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
log ">>> Starting nginx"
|
||||||
|
nginx &
|
159
scripts/exec-sshuttle
Executable file
159
scripts/exec-sshuttle
Executable file
@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export MSYS_NO_PATHCONV=1
|
||||||
|
|
||||||
|
function with_set_x() {
|
||||||
|
set -x
|
||||||
|
"$@"
|
||||||
|
{
|
||||||
|
ec=$?
|
||||||
|
set +x
|
||||||
|
return $ec
|
||||||
|
} 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
echo "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_cmd='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
|
||||||
|
ssh_copy_id=false
|
||||||
|
args=()
|
||||||
|
subnet_args=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
arg=$1
|
||||||
|
shift
|
||||||
|
case "$arg" in
|
||||||
|
-v|-vv*)
|
||||||
|
ssh_cmd+=" -v"
|
||||||
|
args+=("$arg")
|
||||||
|
;;
|
||||||
|
-r)
|
||||||
|
args+=("-r" "$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--copy-id)
|
||||||
|
ssh_copy_id=true
|
||||||
|
;;
|
||||||
|
--server-py=*)
|
||||||
|
server_pyenv_ver="${arg#*=}"
|
||||||
|
;;
|
||||||
|
--client-py=*)
|
||||||
|
client_pyenv_ver="${arg#*=}"
|
||||||
|
;;
|
||||||
|
-6)
|
||||||
|
ipv6_only=true
|
||||||
|
;;
|
||||||
|
--sshuttle-bin=*)
|
||||||
|
sshuttle_bin="${arg#*=}"
|
||||||
|
;;
|
||||||
|
-N|*/*)
|
||||||
|
subnet_args+=("$arg")
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
args+=("$arg")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
target=$arg
|
||||||
|
else
|
||||||
|
args+=("$arg")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ ${#subnet_args[@]} -eq 0 ]]; then
|
||||||
|
subnet_args=("-N")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $target == node-* ]]; then
|
||||||
|
log "Target is a a test-bed node"
|
||||||
|
port="2222"
|
||||||
|
user_part="test:test"
|
||||||
|
host=$("$(dirname "$0")/test-bed" get-ip "$target")
|
||||||
|
index=${target#node-}
|
||||||
|
if [[ $ipv6_only == true ]]; then
|
||||||
|
args+=("2001:0DB8::/112")
|
||||||
|
else
|
||||||
|
args+=("10.55.$index.0/24")
|
||||||
|
fi
|
||||||
|
target="$user_part@$host:$port"
|
||||||
|
if ! command -v sshpass >/dev/null; then
|
||||||
|
log "sshpass is not found. You might have to manually enter ssh password: 'test'"
|
||||||
|
fi
|
||||||
|
if [[ -z $server_pyenv_ver ]]; then
|
||||||
|
log "server-py argumwnt is not specified. Setting it to 3.8"
|
||||||
|
server_pyenv_ver="3.8"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n $server_pyenv_ver ]]; then
|
||||||
|
log "Would pass PYENV_VERRSION=$server_pyenv_ver to server. pyenv is required on server to make it work"
|
||||||
|
pycmd="/pyenv/shims/python"
|
||||||
|
ssh_cmd+=" -o SetEnv=PYENV_VERSION=${server_pyenv_ver:-'3'}"
|
||||||
|
args=("--python=$pycmd" "${args[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $ssh_copy_id == true ]]; then
|
||||||
|
log "Trying to make it passwordless"
|
||||||
|
if [[ $target == *@* ]]; then
|
||||||
|
user_part="${target%%@*}"
|
||||||
|
host_part="${target#*@}"
|
||||||
|
else
|
||||||
|
user_part="$(whoami)"
|
||||||
|
host_part="$target"
|
||||||
|
fi
|
||||||
|
if [[ $host_part == *:* ]]; then
|
||||||
|
host="${host_part%:*}"
|
||||||
|
port="${host_part#*:}"
|
||||||
|
else
|
||||||
|
host="$host_part"
|
||||||
|
port="22"
|
||||||
|
fi
|
||||||
|
if [[ $user_part == *:* ]]; then
|
||||||
|
user="${user_part%:*}"
|
||||||
|
password="${user_part#*:}"
|
||||||
|
else
|
||||||
|
user="$user_part"
|
||||||
|
password=""
|
||||||
|
fi
|
||||||
|
cmd=(ssh-copy-id -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "$port" "$user@$host")
|
||||||
|
if [[ -n $password ]] && command -v sshpass >/dev/null; then
|
||||||
|
cmd=(sshpass -p "$password" "${cmd[@]}")
|
||||||
|
fi
|
||||||
|
with_set_x "${cmd[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z $sshuttle_bin || "$sshuttle_bin" == dev ]]; then
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
export PYTHONPATH="."
|
||||||
|
if [[ -n $client_pyenv_ver ]]; then
|
||||||
|
log "Using pyenv version: $client_pyenv_ver"
|
||||||
|
command -v pyenv &>/dev/null || log "You have to install pyenv to use --client-py" && exit 1
|
||||||
|
sshuttle_cmd=(/usr/bin/env PYENV_VERSION="$client_pyenv_ver" pyenv exec python -m sshuttle)
|
||||||
|
else
|
||||||
|
log "Using best python version availble"
|
||||||
|
if [ -x "$(command -v python3)" ] &&
|
||||||
|
python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
|
||||||
|
sshuttle_cmd=(python3 -m sshuttle)
|
||||||
|
else
|
||||||
|
sshuttle_cmd=(python -m sshuttle)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
[[ -n $client_pyenv_ver ]] && log "Can't specify --client-py when --sshuttle-bin is specified" && exit 1
|
||||||
|
sshuttle_cmd=("$sshuttle_bin")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ " ${args[*]} " != *" --ssh-cmd "* ]]; then
|
||||||
|
args=("--ssh-cmd" "$ssh_cmd" "${args[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ " ${args[*]} " != *" -r "* ]]; then
|
||||||
|
args=("-r" "$target" "${args[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -x
|
||||||
|
"${sshuttle_cmd[@]}" --version
|
||||||
|
exec "${sshuttle_cmd[@]}" "${args[@]}" "${subnet_args[@]}"
|
86
scripts/exec-tool
Executable file
86
scripts/exec-tool
Executable file
@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
|
||||||
|
function with_set_x() {
|
||||||
|
set -x
|
||||||
|
"$@"
|
||||||
|
{
|
||||||
|
ec=$?
|
||||||
|
set +x
|
||||||
|
return $ec
|
||||||
|
} 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
echo "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
args=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
arg=$1
|
||||||
|
shift
|
||||||
|
case "$arg" in
|
||||||
|
-6)
|
||||||
|
ipv6_only=true
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
-*) ;;
|
||||||
|
*)
|
||||||
|
if [[ -z $tool ]]; then
|
||||||
|
tool=$arg
|
||||||
|
continue
|
||||||
|
elif [[ -z $node ]]; then
|
||||||
|
node=$arg
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
args+=("$arg")
|
||||||
|
done
|
||||||
|
|
||||||
|
tool=${tool?:"tool argument missing. should be one of iperf3,ping,curl,ab"}
|
||||||
|
node=${node?:"node argument missing. should be 'node-1' , 'node-2' etc"}
|
||||||
|
|
||||||
|
if [[ $node == node-* ]]; then
|
||||||
|
index=${node#node-}
|
||||||
|
if [[ $ipv6_only == true ]]; then
|
||||||
|
host="2001:0DB8::55$index"
|
||||||
|
else
|
||||||
|
host="10.55.$index.77"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
host=$node
|
||||||
|
fi
|
||||||
|
|
||||||
|
connect_timeout_sec=3
|
||||||
|
|
||||||
|
case "$tool" in
|
||||||
|
ping)
|
||||||
|
with_set_x exec ping -W $connect_timeout_sec "${args[@]}" "$host"
|
||||||
|
;;
|
||||||
|
iperf3)
|
||||||
|
port=5001
|
||||||
|
with_set_x exec iperf3 --client "$host" --port=$port --connect-timeout=$((connect_timeout_sec * 1000)) "${args[@]}"
|
||||||
|
;;
|
||||||
|
curl)
|
||||||
|
port=8080
|
||||||
|
if [[ $host = *:* ]]; then
|
||||||
|
host="[$host]"
|
||||||
|
args+=(--ipv6)
|
||||||
|
fi
|
||||||
|
with_set_x exec curl "http://$host:$port/" -v --connect-timeout $connect_timeout_sec "${args[@]}"
|
||||||
|
;;
|
||||||
|
ab)
|
||||||
|
port=8080
|
||||||
|
if [[ " ${args[*]}" != *" -n "* && " ${args[*]}" != *" -c "* ]]; then
|
||||||
|
args+=(-n 500 -c 50 "${args[@]}")
|
||||||
|
fi
|
||||||
|
with_set_x exec ab -s $connect_timeout_sec "${args[@]}" "http://$host:$port/"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log "Unknown tool: $tool"
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
40
scripts/run-benchmark
Executable file
40
scripts/run-benchmark
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
function with_set_x() {
|
||||||
|
set -x
|
||||||
|
"$@"
|
||||||
|
{
|
||||||
|
ec=$?
|
||||||
|
set +x
|
||||||
|
return $ec
|
||||||
|
} 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
echo "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
./test-bed up -d
|
||||||
|
|
||||||
|
benchmark() {
|
||||||
|
log -e "\n======== Benchmarking sshuttle | Args: [$*] ========"
|
||||||
|
local node=$1
|
||||||
|
shift
|
||||||
|
with_set_x ./exec-sshuttle "$node" --listen 55771 "$@" &
|
||||||
|
sshuttle_pid=$!
|
||||||
|
trap 'kill -0 $sshuttle_pid &>/dev/null && kill -15 $sshuttle_pid' EXIT
|
||||||
|
while ! nc -z localhost 55771; do sleep 0.1; done
|
||||||
|
sleep 1
|
||||||
|
./exec-tool iperf3 "$node" --time=4
|
||||||
|
with_set_x kill -15 $sshuttle_pid
|
||||||
|
wait $sshuttle_pid || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
benchmark "${@}"
|
||||||
|
else
|
||||||
|
benchmark node-1 --sshuttle-bin="${SSHUTTLE_BIN:-sshuttle}"
|
||||||
|
benchmark node-1 --sshuttle-bin=dev
|
||||||
|
fi
|
9
scripts/run-checks
Executable file
9
scripts/run-checks
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
export PYTHONPATH=.
|
||||||
|
|
||||||
|
set -x
|
||||||
|
python -m flake8 sshuttle tests
|
||||||
|
python -m pytest .
|
42
scripts/test-bed
Executable file
42
scripts/test-bed
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
if [[ -z $1 || $1 = -* ]]; then
|
||||||
|
set -- up "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
function with_set_x() {
|
||||||
|
set -x
|
||||||
|
"$@"
|
||||||
|
{
|
||||||
|
ec=$?
|
||||||
|
set +x
|
||||||
|
return $ec
|
||||||
|
} 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
# podman build -t ghcr.io/sshuttle/sshuttle-testbed .
|
||||||
|
with_set_x docker build --progress=plain -t ghcr.io/sshuttle/sshuttle-testbed -f Containerfile .
|
||||||
|
}
|
||||||
|
|
||||||
|
function compose() {
|
||||||
|
# podman-compose "$@"
|
||||||
|
with_set_x docker compose "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
function get-ip() {
|
||||||
|
local container_name=sshuttle-testbed-"$1"
|
||||||
|
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $1 == get-ip ]]; then
|
||||||
|
shift
|
||||||
|
get-ip "$@"
|
||||||
|
else
|
||||||
|
if [[ $* = *--build* ]]; then
|
||||||
|
build
|
||||||
|
fi
|
||||||
|
compose "$@"
|
||||||
|
fi
|
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 1.1.2
|
current_version = 1.3.1
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
[bumpversion:file:setup.py]
|
||||||
|
|
||||||
@ -23,5 +23,8 @@ show-source = true
|
|||||||
statistics = true
|
statistics = true
|
||||||
max-line-length = 128
|
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
|
||||||
|
61
setup.py
61
setup.py
@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# Copyright 2012-2014 Brian May
|
|
||||||
#
|
|
||||||
# This file is part of sshuttle.
|
|
||||||
#
|
|
||||||
# sshuttle is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as
|
|
||||||
# published by the Free Software Foundation; either version 2.1 of
|
|
||||||
# the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# sshuttle is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
|
||||||
# along with sshuttle; If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="sshuttle",
|
|
||||||
version='1.1.2',
|
|
||||||
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.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Topic :: System :: Networking",
|
|
||||||
],
|
|
||||||
entry_points={
|
|
||||||
'console_scripts': [
|
|
||||||
'sshuttle = sshuttle.cmdline:main',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
python_requires='>=3.8',
|
|
||||||
install_requires=[
|
|
||||||
],
|
|
||||||
tests_require=[
|
|
||||||
'pytest',
|
|
||||||
'pytest-cov',
|
|
||||||
'pytest-runner',
|
|
||||||
'flake8',
|
|
||||||
],
|
|
||||||
keywords="ssh vpn",
|
|
||||||
)
|
|
@ -1,4 +1 @@
|
|||||||
try:
|
__version__ = "1.3.1"
|
||||||
from sshuttle.version import version as __version__
|
|
||||||
except ImportError:
|
|
||||||
__version__ = "unknown"
|
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
"""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
|
||||||
sys.exit(main())
|
from sshuttle.helpers import debug3
|
||||||
|
|
||||||
|
debug3("Start: (pid=%s, ppid=%s) %r" % (os.getpid(), os.getppid(), sys.argv))
|
||||||
|
exit_code = main()
|
||||||
|
debug3("Exit: (pid=%s, ppid=%s, code=%s) cmd %r" % (os.getpid(), os.getppid(), exit_code, sys.argv))
|
||||||
|
sys.exit(exit_code)
|
||||||
|
@ -3,24 +3,27 @@ import zlib
|
|||||||
import types
|
import types
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
verbosity = verbosity # noqa: F821 must be a previously defined global
|
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:
|
if verbosity > 0:
|
||||||
sys.stderr.write(' s: Running server on remote host with %s (version %s)\n'
|
sys.stderr.write(' s: Running server on remote host with %s (version %s)\n'
|
||||||
% (sys.executable, platform.python_version()))
|
% (sys.executable, platform.python_version()))
|
||||||
|
|
||||||
z = zlib.decompressobj()
|
z = zlib.decompressobj()
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
name = sys.stdin.readline().strip()
|
name = stdin.readline().strip()
|
||||||
if name:
|
if name:
|
||||||
# python2 compat: in python2 sys.stdin.readline().strip() -> str
|
# python2 compat: in python2 stdin.readline().strip() -> str
|
||||||
# in python3 sys.stdin.readline().strip() -> bytes
|
# in python3 stdin.readline().strip() -> bytes
|
||||||
# (see #481)
|
# (see #481)
|
||||||
if sys.version_info >= (3, 0):
|
if sys.version_info >= (3, 0):
|
||||||
name = name.decode("ASCII")
|
name = name.decode("ASCII")
|
||||||
nbytes = int(sys.stdin.readline())
|
nbytes = int(stdin.readline())
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
sys.stderr.write(' s: assembling %r (%d bytes)\n'
|
sys.stderr.write(' s: assembling %r (%d bytes)\n'
|
||||||
% (name, nbytes))
|
% (name, nbytes))
|
||||||
content = z.decompress(sys.stdin.read(nbytes))
|
content = z.decompress(stdin.read(nbytes))
|
||||||
|
|
||||||
module = types.ModuleType(name)
|
module = types.ModuleType(name)
|
||||||
parents = name.rsplit(".", 1)
|
parents = name.rsplit(".", 1)
|
||||||
@ -44,6 +47,7 @@ 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.latency_buffer_size,
|
main(options.latency_control, options.latency_buffer_size,
|
||||||
options.auto_hosts, options.to_nameserver,
|
options.auto_hosts, options.to_nameserver,
|
||||||
options.auto_nets)
|
options.auto_nets)
|
||||||
|
@ -5,6 +5,7 @@ import time
|
|||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import base64
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
@ -14,7 +15,7 @@ import sshuttle.ssyslog as ssyslog
|
|||||||
import sshuttle.sdnotify as sdnotify
|
import sshuttle.sdnotify as sdnotify
|
||||||
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
||||||
resolvconf_nameservers, which
|
resolvconf_nameservers, which, is_admin_user, RWPair
|
||||||
from sshuttle.methods import get_method, Features
|
from sshuttle.methods import get_method, Features
|
||||||
from sshuttle import __version__
|
from sshuttle import __version__
|
||||||
try:
|
try:
|
||||||
@ -209,7 +210,10 @@ class FirewallClient:
|
|||||||
def __init__(self, method_name, sudo_pythonpath):
|
def __init__(self, method_name, sudo_pythonpath):
|
||||||
self.auto_nets = []
|
self.auto_nets = []
|
||||||
|
|
||||||
argvbase = ([sys.executable, sys.argv[0]] +
|
argv0 = sys.argv[0]
|
||||||
|
# argv0 is either be a normal Python file or an executable.
|
||||||
|
# After installed as a package, sshuttle command points to an .exe in Windows and Python shebang script elsewhere.
|
||||||
|
argvbase = (([sys.executable, sys.argv[0]] if argv0.endswith('.py') else [argv0]) +
|
||||||
['-v'] * (helpers.verbose or 0) +
|
['-v'] * (helpers.verbose or 0) +
|
||||||
['--method', method_name] +
|
['--method', method_name] +
|
||||||
['--firewall'])
|
['--firewall'])
|
||||||
@ -219,9 +223,18 @@ class FirewallClient:
|
|||||||
# A list of commands that we can try to run to start the firewall.
|
# A list of commands that we can try to run to start the firewall.
|
||||||
argv_tries = []
|
argv_tries = []
|
||||||
|
|
||||||
if os.getuid() == 0: # No need to elevate privileges
|
if is_admin_user(): # No need to elevate privileges
|
||||||
argv_tries.append(argvbase)
|
argv_tries.append(argvbase)
|
||||||
else:
|
else:
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# runas_path = which("runas")
|
||||||
|
# if runas_path:
|
||||||
|
# argv_tries.append([runas_path , '/noprofile', '/user:Administrator', 'python'])
|
||||||
|
# XXX: Attempt to elevate privilege using 'runas' in windows seems not working.
|
||||||
|
# Because underlying ShellExecute() Windows api does not allow child process to inherit stdio.
|
||||||
|
# TODO(nom3ad): Try to implement another way to achieve this.
|
||||||
|
raise Fatal("Privilege elevation for Windows is not yet implemented. Please run from an administrator shell")
|
||||||
|
|
||||||
# Linux typically uses sudo; OpenBSD uses doas. However, some
|
# Linux typically uses sudo; OpenBSD uses doas. However, some
|
||||||
# Linux distributions are starting to use doas.
|
# Linux distributions are starting to use doas.
|
||||||
sudo_cmd = ['sudo', '-p', '[local sudo] Password: ']
|
sudo_cmd = ['sudo', '-p', '[local sudo] Password: ']
|
||||||
@ -254,8 +267,7 @@ class FirewallClient:
|
|||||||
|
|
||||||
# If we can find doas and not sudo or if we are on
|
# If we can find doas and not sudo or if we are on
|
||||||
# OpenBSD, try using doas first.
|
# OpenBSD, try using doas first.
|
||||||
if (doas_path and not sudo_path) or \
|
if (doas_path and not sudo_path) or platform.platform().startswith('OpenBSD'):
|
||||||
platform.platform().startswith('OpenBSD'):
|
|
||||||
argv_tries = [doas_cmd, sudo_cmd, argvbase]
|
argv_tries = [doas_cmd, sudo_cmd, argvbase]
|
||||||
else:
|
else:
|
||||||
argv_tries = [sudo_cmd, doas_cmd, argvbase]
|
argv_tries = [sudo_cmd, doas_cmd, argvbase]
|
||||||
@ -265,21 +277,58 @@ class FirewallClient:
|
|||||||
# successful, set 'success' variable and break.
|
# successful, set 'success' variable and break.
|
||||||
success = False
|
success = False
|
||||||
for argv in argv_tries:
|
for argv in argv_tries:
|
||||||
# we can't use stdin/stdout=subprocess.PIPE here, as we
|
|
||||||
# normally would, because stupid Linux 'su' requires that
|
|
||||||
# stdin be attached to a tty. Instead, attach a
|
|
||||||
# *bidirectional* socket to its stdout, and use that for
|
|
||||||
# talking in both directions.
|
|
||||||
(s1, s2) = socket.socketpair()
|
|
||||||
|
|
||||||
def setup():
|
if sys.platform != 'win32':
|
||||||
# run in the child process
|
# we can't use stdin/stdout=subprocess.PIPE here, as we
|
||||||
s2.close()
|
# normally would, because stupid Linux 'su' requires that
|
||||||
|
# stdin be attached to a tty. Instead, attach a
|
||||||
|
# *bidirectional* socket to its stdout, and use that for
|
||||||
|
# talking in both directions.
|
||||||
|
(s1, s2) = socket.socketpair()
|
||||||
|
pstdout = s1
|
||||||
|
pstdin = s1
|
||||||
|
penv = None
|
||||||
|
|
||||||
|
def preexec_fn():
|
||||||
|
# run in the child process
|
||||||
|
s2.close()
|
||||||
|
|
||||||
|
def get_pfile():
|
||||||
|
s1.close()
|
||||||
|
return s2.makefile('rwb')
|
||||||
|
|
||||||
|
else:
|
||||||
|
# In Windows CPython, BSD sockets are not supported as subprocess stdio.
|
||||||
|
# if client (and firewall) processes is running as admin user, pipe based stdio can be used for communication.
|
||||||
|
# But if firewall process is spwaned in elevated mode by non-admin client process, access to stdio is lost.
|
||||||
|
# To work around this, we can use a socketpair.
|
||||||
|
# But socket need to be "shared" to child process as it can't be directly set as stdio.
|
||||||
|
can_use_stdio = is_admin_user()
|
||||||
|
|
||||||
|
preexec_fn = None
|
||||||
|
penv = os.environ.copy()
|
||||||
|
if can_use_stdio:
|
||||||
|
pstdout = ssubprocess.PIPE
|
||||||
|
pstdin = ssubprocess.PIPE
|
||||||
|
|
||||||
|
def get_pfile():
|
||||||
|
return RWPair(self.p.stdout, self.p.stdin)
|
||||||
|
penv['SSHUTTLE_FW_COM_CHANNEL'] = 'stdio'
|
||||||
|
else:
|
||||||
|
pstdout = None
|
||||||
|
pstdin = None
|
||||||
|
(s1, s2) = socket.socketpair()
|
||||||
|
socket_share_data = s1.share(self.p.pid)
|
||||||
|
socket_share_data_b64 = base64.b64encode(socket_share_data)
|
||||||
|
penv['SSHUTTLE_FW_COM_CHANNEL'] = socket_share_data_b64
|
||||||
|
|
||||||
|
def get_pfile():
|
||||||
|
s1.close()
|
||||||
|
return s2.makefile('rwb')
|
||||||
try:
|
try:
|
||||||
debug1("Starting firewall manager with command: %r" % argv)
|
debug1("Starting firewall manager with command: %r" % argv)
|
||||||
self.p = ssubprocess.Popen(argv, stdout=s1, stdin=s1,
|
self.p = ssubprocess.Popen(argv, stdout=pstdout, stdin=pstdin, env=penv,
|
||||||
preexec_fn=setup)
|
preexec_fn=preexec_fn)
|
||||||
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
# This exception will occur if the program isn't
|
# This exception will occur if the program isn't
|
||||||
@ -287,11 +336,14 @@ class FirewallClient:
|
|||||||
debug1('Unable to start firewall manager. Popen failed. '
|
debug1('Unable to start firewall manager. Popen failed. '
|
||||||
'Command=%r Exception=%s' % (argv, e))
|
'Command=%r Exception=%s' % (argv, e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.argv = argv
|
self.argv = argv
|
||||||
s1.close()
|
self.pfile = get_pfile()
|
||||||
self.pfile = s2.makefile('rwb')
|
|
||||||
line = self.pfile.readline()
|
try:
|
||||||
|
line = self.pfile.readline()
|
||||||
|
except IOError:
|
||||||
|
# happens when firewall subprocess exists
|
||||||
|
line = ''
|
||||||
|
|
||||||
rv = self.p.poll() # Check if process is still running
|
rv = self.p.poll() # Check if process is still running
|
||||||
if rv:
|
if rv:
|
||||||
@ -334,7 +386,7 @@ class FirewallClient:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
raise Fatal("All attempts to elevate privileges failed.")
|
raise Fatal("All attempts to run firewall client process with elevated privileges were failed.")
|
||||||
|
|
||||||
def setup(self, subnets_include, subnets_exclude, nslist,
|
def setup(self, subnets_include, subnets_exclude, nslist,
|
||||||
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
|
||||||
@ -539,7 +591,7 @@ def ondns(listener, method, mux, handlers):
|
|||||||
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control, latency_buffer_size,
|
python, latency_control, latency_buffer_size,
|
||||||
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
|
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
|
||||||
to_nameserver):
|
to_nameserver, add_cmd_delimiter, remote_shell):
|
||||||
|
|
||||||
helpers.logprefix = 'c : '
|
helpers.logprefix = 'c : '
|
||||||
debug1('Starting client with Python version %s'
|
debug1('Starting client with Python version %s'
|
||||||
@ -551,9 +603,11 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
debug1('Connecting to server...')
|
debug1('Connecting to server...')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(serverproc, serversock) = ssh.connect(
|
(serverproc, rfile, wfile) = ssh.connect(
|
||||||
ssh_cmd, remotename, python,
|
ssh_cmd, remotename, python,
|
||||||
stderr=ssyslog._p and ssyslog._p.stdin,
|
stderr=ssyslog._p and ssyslog._p.stdin,
|
||||||
|
add_cmd_delimiter=add_cmd_delimiter,
|
||||||
|
remote_shell=remote_shell,
|
||||||
options=dict(latency_control=latency_control,
|
options=dict(latency_control=latency_control,
|
||||||
latency_buffer_size=latency_buffer_size,
|
latency_buffer_size=latency_buffer_size,
|
||||||
auto_hosts=auto_hosts,
|
auto_hosts=auto_hosts,
|
||||||
@ -561,24 +615,25 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
auto_nets=auto_nets))
|
auto_nets=auto_nets))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.EPIPE:
|
if e.args[0] == errno.EPIPE:
|
||||||
|
debug3('Error: EPIPE: ' + repr(e))
|
||||||
raise Fatal("failed to establish ssh session (1)")
|
raise Fatal("failed to establish ssh session (1)")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
|
mux = Mux(rfile, wfile)
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
|
|
||||||
expected = b'SSHUTTLE0001'
|
expected = b'SSHUTTLE0001'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v = 'x'
|
v = 'x'
|
||||||
while v and v != b'\0':
|
while v and v != b'\0':
|
||||||
v = serversock.recv(1)
|
v = rfile.read(1)
|
||||||
v = 'x'
|
v = 'x'
|
||||||
while v and v != b'\0':
|
while v and v != b'\0':
|
||||||
v = serversock.recv(1)
|
v = rfile.read(1)
|
||||||
initstring = serversock.recv(len(expected))
|
initstring = rfile.read(len(expected))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.ECONNRESET:
|
if e.args[0] == errno.ECONNRESET:
|
||||||
|
debug3('Error: ECONNRESET ' + repr(e))
|
||||||
raise Fatal("failed to establish ssh session (2)")
|
raise Fatal("failed to establish ssh session (2)")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
@ -755,7 +810,7 @@ def main(listenip_v6, listenip_v4,
|
|||||||
latency_buffer_size, dns, nslist,
|
latency_buffer_size, dns, nslist,
|
||||||
method_name, seed_hosts, auto_hosts, auto_nets,
|
method_name, seed_hosts, auto_hosts, auto_nets,
|
||||||
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
||||||
user, group, sudo_pythonpath, tmark):
|
user, group, sudo_pythonpath, add_cmd_delimiter, remote_shell, tmark):
|
||||||
|
|
||||||
if not remotename:
|
if not remotename:
|
||||||
raise Fatal("You must use -r/--remote to specify a remote "
|
raise Fatal("You must use -r/--remote to specify a remote "
|
||||||
@ -820,7 +875,8 @@ def main(listenip_v6, listenip_v4,
|
|||||||
|
|
||||||
# listenip_v4 contains user specified value or it is set to "auto".
|
# listenip_v4 contains user specified value or it is set to "auto".
|
||||||
if listenip_v4 == "auto":
|
if listenip_v4 == "auto":
|
||||||
listenip_v4 = ('127.0.0.1', 0)
|
listenip_v4 = ('127.0.0.1' if avail.loopback_proxy_port else '0.0.0.0', 0)
|
||||||
|
debug1("Using default IPv4 listen address " + listenip_v4[0])
|
||||||
|
|
||||||
# listenip_v6 is...
|
# listenip_v6 is...
|
||||||
# None when IPv6 is disabled.
|
# None when IPv6 is disabled.
|
||||||
@ -830,8 +886,8 @@ def main(listenip_v6, listenip_v4,
|
|||||||
debug1("IPv6 disabled by --disable-ipv6")
|
debug1("IPv6 disabled by --disable-ipv6")
|
||||||
if listenip_v6 == "auto":
|
if listenip_v6 == "auto":
|
||||||
if avail.ipv6:
|
if avail.ipv6:
|
||||||
debug1("IPv6 enabled: Using default IPv6 listen address ::1")
|
listenip_v6 = ('::1' if avail.loopback_proxy_port else '::', 0)
|
||||||
listenip_v6 = ('::1', 0)
|
debug1("IPv6 enabled: Using default IPv6 listen address " + listenip_v6[0])
|
||||||
else:
|
else:
|
||||||
debug1("IPv6 disabled since it isn't supported by method "
|
debug1("IPv6 disabled since it isn't supported by method "
|
||||||
"%s." % fw.method.name)
|
"%s." % fw.method.name)
|
||||||
@ -1103,7 +1159,7 @@ def main(listenip_v6, listenip_v4,
|
|||||||
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control, latency_buffer_size,
|
python, latency_control, latency_buffer_size,
|
||||||
dns_listener, seed_hosts, auto_hosts, auto_nets,
|
dns_listener, seed_hosts, auto_hosts, auto_nets,
|
||||||
daemon, to_nameserver)
|
daemon, to_nameserver, add_cmd_delimiter, remote_shell)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
if daemon:
|
if daemon:
|
||||||
|
@ -11,6 +11,7 @@ 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():
|
||||||
@ -37,6 +38,16 @@ 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')
|
||||||
@ -115,6 +126,8 @@ def main():
|
|||||||
opt.user,
|
opt.user,
|
||||||
opt.group,
|
opt.group,
|
||||||
opt.sudo_pythonpath,
|
opt.sudo_pythonpath,
|
||||||
|
opt.add_cmd_delimiter,
|
||||||
|
opt.remote_shell,
|
||||||
opt.tmark)
|
opt.tmark)
|
||||||
|
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
|
@ -7,13 +7,18 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import traceback
|
import traceback
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
import sshuttle.ssyslog as ssyslog
|
import sshuttle.ssyslog as ssyslog
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
from sshuttle.helpers import log, 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
|
||||||
|
|
||||||
HOSTSFILE = '/etc/hosts'
|
if sys.platform == 'win32':
|
||||||
|
HOSTSFILE = r"C:\Windows\System32\drivers\etc\hosts"
|
||||||
|
else:
|
||||||
|
HOSTSFILE = '/etc/hosts'
|
||||||
sshuttle_pid = None
|
sshuttle_pid = None
|
||||||
|
|
||||||
|
|
||||||
@ -46,12 +51,13 @@ 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 st is not None:
|
if sys.platform != 'win32':
|
||||||
os.chown(tmpname, st.st_uid, st.st_gid)
|
if st is not None:
|
||||||
os.chmod(tmpname, st.st_mode)
|
os.chown(tmpname, st.st_uid, st.st_gid)
|
||||||
else:
|
os.chmod(tmpname, st.st_mode)
|
||||||
os.chown(tmpname, 0, 0)
|
else:
|
||||||
os.chmod(tmpname, 0o644)
|
os.chown(tmpname, 0, 0)
|
||||||
|
os.chmod(tmpname, 0o644)
|
||||||
try:
|
try:
|
||||||
os.rename(tmpname, HOSTSFILE)
|
os.rename(tmpname, HOSTSFILE)
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -82,14 +88,17 @@ def firewall_exit(signum, frame):
|
|||||||
# the typical exit process as described above.
|
# the typical exit process as described above.
|
||||||
global sshuttle_pid
|
global sshuttle_pid
|
||||||
if sshuttle_pid:
|
if sshuttle_pid:
|
||||||
debug1("Relaying SIGINT to sshuttle process %d\n" % sshuttle_pid)
|
debug1("Relaying interupt signal to sshuttle process %d" % sshuttle_pid)
|
||||||
os.kill(sshuttle_pid, signal.SIGINT)
|
if sys.platform == 'win32':
|
||||||
|
sig = signal.CTRL_C_EVENT
|
||||||
|
else:
|
||||||
|
sig = signal.SIGINT
|
||||||
|
os.kill(sshuttle_pid, sig)
|
||||||
|
|
||||||
|
|
||||||
# Isolate function that needs to be replaced for tests
|
def _setup_daemon_for_unix_like():
|
||||||
def setup_daemon():
|
if not is_admin_user():
|
||||||
if os.getuid() != 0:
|
raise Fatal('You must have root privileges (or enable su/sudo) to set the firewall')
|
||||||
raise Fatal('You must be root (or enable su/sudo) to set the firewall')
|
|
||||||
|
|
||||||
# don't disappear if our controlling terminal or stdout/stderr
|
# don't disappear if our controlling terminal or stdout/stderr
|
||||||
# disappears; we still have to clean up.
|
# disappears; we still have to clean up.
|
||||||
@ -110,7 +119,34 @@ def setup_daemon():
|
|||||||
# setsid() fails if sudo is configured with the use_pty option.
|
# setsid() fails if sudo is configured with the use_pty option.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return sys.stdin, sys.stdout
|
return sys.stdin.buffer, sys.stdout.buffer
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
@ -184,28 +220,42 @@ def main(method_name, syslog):
|
|||||||
"PATH." % method_name)
|
"PATH." % method_name)
|
||||||
|
|
||||||
debug1('ready method name %s.' % method.name)
|
debug1('ready method name %s.' % method.name)
|
||||||
stdout.write('READY %s\n' % 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).
|
||||||
line = stdin.readline(128)
|
try:
|
||||||
if not line:
|
line = _read_next_string_line()
|
||||||
return # parent died; nothing to do
|
if not line:
|
||||||
|
return # parent probably exited
|
||||||
|
except IOError as e:
|
||||||
|
# On windows, ConnectionResetError is thrown when parent process closes it's socket pair end
|
||||||
|
debug3('read from stdin failed: %s' % (e,))
|
||||||
|
return
|
||||||
|
|
||||||
subnets = []
|
subnets = []
|
||||||
if line != 'ROUTES\n':
|
if line != 'ROUTES':
|
||||||
raise Fatal('expected ROUTES but got %r' % line)
|
raise Fatal('expected ROUTES but got %r' % line)
|
||||||
while 1:
|
while 1:
|
||||||
line = stdin.readline(128)
|
line = _read_next_string_line()
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('expected route but got %r' % line)
|
raise Fatal('expected route but got %r' % line)
|
||||||
elif line.startswith("NSLIST\n"):
|
elif line.startswith("NSLIST"):
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, width, exclude, ip, fport, lport) = \
|
(family, width, exclude, ip, fport, lport) = line.split(',', 5)
|
||||||
line.strip().split(',', 5)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
raise Fatal('expected route or NSLIST but got %r' % line)
|
raise Fatal('expected route or NSLIST but got %r' % line)
|
||||||
subnets.append((
|
subnets.append((
|
||||||
@ -218,16 +268,16 @@ def main(method_name, syslog):
|
|||||||
debug2('Got subnets: %r' % subnets)
|
debug2('Got subnets: %r' % subnets)
|
||||||
|
|
||||||
nslist = []
|
nslist = []
|
||||||
if line != 'NSLIST\n':
|
if line != 'NSLIST':
|
||||||
raise Fatal('expected NSLIST but got %r' % line)
|
raise Fatal('expected NSLIST but got %r' % line)
|
||||||
while 1:
|
while 1:
|
||||||
line = stdin.readline(128)
|
line = _read_next_string_line()
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('expected nslist but got %r' % line)
|
raise Fatal('expected nslist but got %r' % line)
|
||||||
elif line.startswith("PORTS "):
|
elif line.startswith("PORTS "):
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, ip) = line.strip().split(',', 1)
|
(family, ip) = line.split(',', 1)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise Fatal('expected nslist or PORTS but got %r' % line)
|
raise Fatal('expected nslist or PORTS but got %r' % line)
|
||||||
nslist.append((int(family), ip))
|
nslist.append((int(family), ip))
|
||||||
@ -257,15 +307,13 @@ def main(method_name, syslog):
|
|||||||
debug2('Got ports: %d,%d,%d,%d'
|
debug2('Got ports: %d,%d,%d,%d'
|
||||||
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
||||||
|
|
||||||
line = stdin.readline(128)
|
line = _read_next_string_line()
|
||||||
if not line:
|
if not line or not line.startswith("GO "):
|
||||||
raise Fatal('expected GO but got %r' % line)
|
|
||||||
elif not line.startswith("GO "):
|
|
||||||
raise Fatal('expected GO but got %r' % line)
|
raise Fatal('expected GO but got %r' % line)
|
||||||
|
|
||||||
_, _, args = line.partition(" ")
|
_, _, args = line.partition(" ")
|
||||||
global sshuttle_pid
|
global sshuttle_pid
|
||||||
udp, user, group, tmark, sshuttle_pid = args.strip().split(" ", 4)
|
udp, user, group, tmark, sshuttle_pid = args.split(" ", 4)
|
||||||
udp = bool(int(udp))
|
udp = bool(int(udp))
|
||||||
sshuttle_pid = int(sshuttle_pid)
|
sshuttle_pid = int(sshuttle_pid)
|
||||||
if user == '-':
|
if user == '-':
|
||||||
@ -297,23 +345,32 @@ def main(method_name, syslog):
|
|||||||
socket.AF_INET, subnets_v4, udp,
|
socket.AF_INET, subnets_v4, udp,
|
||||||
user, group, tmark)
|
user, group, tmark)
|
||||||
|
|
||||||
flush_systemd_dns_cache()
|
try:
|
||||||
stdout.write('STARTED\n')
|
# 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:
|
try:
|
||||||
|
stdout.write(b'STARTED\n')
|
||||||
stdout.flush()
|
stdout.flush()
|
||||||
except IOError:
|
except IOError as e: # the parent process probably died
|
||||||
# the parent process died for some reason; he's surely been loud
|
debug3('write to stdout failed: %s' % (e,))
|
||||||
# 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 = stdin.readline(128)
|
line = _read_next_string_line()
|
||||||
|
if not line:
|
||||||
|
return
|
||||||
if line.startswith('HOST '):
|
if line.startswith('HOST '):
|
||||||
(name, ip) = line[5:].strip().split(',', 1)
|
(name, ip) = line[5:].split(',', 1)
|
||||||
hostmap[name] = ip
|
hostmap[name] = ip
|
||||||
debug2('setting up /etc/hosts.')
|
debug2('setting up /etc/hosts.')
|
||||||
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
|
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
|
||||||
@ -360,11 +417,12 @@ def main(method_name, syslog):
|
|||||||
except Exception:
|
except Exception:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
if sys.platform == 'linux':
|
||||||
flush_systemd_dns_cache()
|
|
||||||
except Exception:
|
|
||||||
try:
|
try:
|
||||||
debug1("Error trying to flush systemd dns cache.")
|
flush_systemd_dns_cache()
|
||||||
debug1(traceback.format_exc())
|
|
||||||
except Exception:
|
except Exception:
|
||||||
debug2("An error occurred, ignoring it.")
|
try:
|
||||||
|
debug1("Error trying to flush systemd dns cache.")
|
||||||
|
debug1(traceback.format_exc())
|
||||||
|
except Exception:
|
||||||
|
debug2("An error occurred, ignoring it.")
|
||||||
|
@ -2,6 +2,13 @@ import sys
|
|||||||
import socket
|
import socket
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import traceback
|
||||||
|
import re
|
||||||
|
|
||||||
|
if sys.platform != "win32":
|
||||||
|
import fcntl
|
||||||
|
|
||||||
logprefix = ''
|
logprefix = ''
|
||||||
verbose = 0
|
verbose = 0
|
||||||
@ -11,10 +18,17 @@ 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
|
||||||
|
pass
|
||||||
|
try:
|
||||||
# Put newline at end of string if line doesn't have one.
|
# Put newline at end of string if line doesn't have one.
|
||||||
if not s.endswith("\n"):
|
if not s.endswith("\n"):
|
||||||
s = s+"\n"
|
s = s+"\n"
|
||||||
@ -25,7 +39,7 @@ def log(s):
|
|||||||
sys.stderr.write(prefix + line + "\n")
|
sys.stderr.write(prefix + line + "\n")
|
||||||
prefix = " "
|
prefix = " "
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
except IOError:
|
except (IOError, ValueError): # ValueError ~ I/O operation on closed file
|
||||||
# this could happen if stderr gets forcibly disconnected, eg. because
|
# 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
|
||||||
@ -102,18 +116,43 @@ def resolvconf_nameservers(systemd_resolved):
|
|||||||
return nsservers
|
return nsservers
|
||||||
|
|
||||||
|
|
||||||
def resolvconf_random_nameserver(systemd_resolved):
|
def windows_nameservers():
|
||||||
|
out = subprocess.check_output(["powershell", "-NonInteractive", "-NoProfile", "-Command", "Get-DnsClientServerAddress"],
|
||||||
|
encoding="utf-8")
|
||||||
|
servers = set()
|
||||||
|
for line in out.splitlines():
|
||||||
|
if line.startswith("Loopback "):
|
||||||
|
continue
|
||||||
|
m = re.search(r'{.+}', line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
for s in m.group().strip('{}').split(','):
|
||||||
|
s = s.strip()
|
||||||
|
if s.startswith('fec0:0:0:ffff'):
|
||||||
|
continue
|
||||||
|
servers.add(s)
|
||||||
|
debug2("Found DNS servers: %s" % servers)
|
||||||
|
return [(socket.AF_INET6 if ':' in s else socket.AF_INET, s) for s in servers]
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_nameserver():
|
||||||
"""Return a random nameserver selected from servers produced by
|
"""Return a random nameserver selected from servers produced by
|
||||||
resolvconf_nameservers(). See documentation for
|
resolvconf_nameservers()/windows_nameservers()
|
||||||
resolvconf_nameservers() for a description of the parameter.
|
|
||||||
"""
|
"""
|
||||||
lines = resolvconf_nameservers(systemd_resolved)
|
if sys.platform == "win32":
|
||||||
if lines:
|
if globals().get('_nameservers') is None:
|
||||||
if len(lines) > 1:
|
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(lines)
|
random.shuffle(ns_list)
|
||||||
return lines[0]
|
return ns_list[0]
|
||||||
else:
|
else:
|
||||||
return (socket.AF_INET, '127.0.0.1')
|
return (socket.AF_INET, '127.0.0.1')
|
||||||
|
|
||||||
@ -220,3 +259,91 @@ def which(file, mode=os.F_OK | os.X_OK):
|
|||||||
else:
|
else:
|
||||||
debug2("which() could not find '%s' in %s" % (file, path))
|
debug2("which() could not find '%s' in %s" % (file, path))
|
||||||
return rv
|
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)
|
||||||
|
@ -18,6 +18,8 @@ CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
|
|||||||
# Have we already failed to write CACHEFILE?
|
# Have we already failed to write CACHEFILE?
|
||||||
CACHE_WRITE_FAILED = False
|
CACHE_WRITE_FAILED = False
|
||||||
|
|
||||||
|
SHOULD_WRITE_CACHE = False
|
||||||
|
|
||||||
hostnames = {}
|
hostnames = {}
|
||||||
queue = {}
|
queue = {}
|
||||||
try:
|
try:
|
||||||
@ -81,6 +83,11 @@ def read_host_cache():
|
|||||||
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):
|
||||||
@ -97,12 +104,13 @@ 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' % (name, ip))
|
||||||
sys.stdout.write('%s,%s\n' % (name, ip))
|
sys.stdout.write('%s,%s\n' % (name, ip))
|
||||||
write_host_cache()
|
SHOULD_WRITE_CACHE = True
|
||||||
|
|
||||||
|
|
||||||
def _check_etc_hosts():
|
def _check_etc_hosts():
|
||||||
|
@ -20,7 +20,7 @@ def ipt_chain_exists(family, table, name):
|
|||||||
argv = [cmd, '-w', '-t', table, '-nL']
|
argv = [cmd, '-w', '-t', table, '-nL']
|
||||||
try:
|
try:
|
||||||
output = ssubprocess.check_output(argv, env=get_env())
|
output = ssubprocess.check_output(argv, env=get_env())
|
||||||
for line in output.decode('ASCII').split('\n'):
|
for line in output.decode('ASCII', errors='replace').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:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
|
import sys
|
||||||
import errno
|
import errno
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from sshuttle.helpers import Fatal, debug3
|
from sshuttle.helpers import Fatal, debug3
|
||||||
@ -45,6 +46,7 @@ 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.ipv4 = True
|
||||||
result.ipv6 = False
|
result.ipv6 = False
|
||||||
result.udp = False
|
result.udp = False
|
||||||
@ -96,6 +98,9 @@ class BaseMethod(object):
|
|||||||
def restore_firewall(self, port, family, udp, user, group):
|
def restore_firewall(self, port, family, udp, user, group):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def wait_for_firewall_ready(self, sshuttle_pid):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def firewall_command(line):
|
def firewall_command(line):
|
||||||
return False
|
return False
|
||||||
@ -109,7 +114,7 @@ def get_method(method_name):
|
|||||||
def get_auto_method():
|
def get_auto_method():
|
||||||
debug3("Selecting a method automatically...")
|
debug3("Selecting a method automatically...")
|
||||||
# Try these methods, in order:
|
# Try these methods, in order:
|
||||||
methods_to_try = ["nat", "nft", "pf", "ipfw"]
|
methods_to_try = ["nat", "nft", "pf", "ipfw"] if sys.platform != "win32" else ["windivert"]
|
||||||
for m in methods_to_try:
|
for m in methods_to_try:
|
||||||
method = get_method(m)
|
method = get_method(m)
|
||||||
if method.is_supported():
|
if method.is_supported():
|
||||||
|
@ -266,7 +266,7 @@ class OpenBsd(Generic):
|
|||||||
("proto_variant", c_uint8),
|
("proto_variant", c_uint8),
|
||||||
("direction", c_uint8)]
|
("direction", c_uint8)]
|
||||||
|
|
||||||
self.pfioc_rule = c_char * 3424
|
self.pfioc_rule = c_char * 3408
|
||||||
self.pfioc_natlook = pfioc_natlook
|
self.pfioc_natlook = pfioc_natlook
|
||||||
super(OpenBsd, self).__init__()
|
super(OpenBsd, self).__init__()
|
||||||
|
|
||||||
|
533
sshuttle/methods/windivert.py
Normal file
533
sshuttle/methods/windivert.py
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from ipaddress import ip_address, ip_network
|
||||||
|
import threading
|
||||||
|
from collections import namedtuple
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
from multiprocessing import shared_memory
|
||||||
|
from struct import Struct
|
||||||
|
from functools import wraps
|
||||||
|
from enum import IntEnum
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
from sshuttle.methods import BaseMethod
|
||||||
|
from sshuttle.helpers import log, debug3, debug1, debug2, get_verbose_level, Fatal
|
||||||
|
|
||||||
|
try:
|
||||||
|
# https://reqrypt.org/windivert-doc.html#divert_iphdr
|
||||||
|
# https://www.reqrypt.org/windivert-changelog.txt
|
||||||
|
import pydivert
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert")
|
||||||
|
|
||||||
|
|
||||||
|
ConnectionTuple = namedtuple(
|
||||||
|
"ConnectionTuple",
|
||||||
|
["protocol", "ip_version", "src_addr", "src_port", "dst_addr", "dst_port", "state_epoch", "state"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
WINDIVERT_MAX_CONNECTIONS = int(os.environ.get('WINDIVERT_MAX_CONNECTIONS', 1024))
|
||||||
|
|
||||||
|
|
||||||
|
class IPProtocol(IntEnum):
|
||||||
|
TCP = socket.IPPROTO_TCP
|
||||||
|
UDP = socket.IPPROTO_UDP
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter(self):
|
||||||
|
return "tcp" if self == IPProtocol.TCP else "udp"
|
||||||
|
|
||||||
|
|
||||||
|
class IPFamily(IntEnum):
|
||||||
|
IPv4 = socket.AF_INET
|
||||||
|
IPv6 = socket.AF_INET6
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_ip_version(version):
|
||||||
|
return IPFamily.IPv6 if version == 4 else IPFamily.IPv4
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter(self):
|
||||||
|
return "ip" if self == socket.AF_INET else "ipv6"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
return 4 if self == socket.AF_INET else 6
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loopback_addr(self):
|
||||||
|
return ip_address("127.0.0.1" if self == socket.AF_INET else "::1")
|
||||||
|
|
||||||
|
|
||||||
|
class ConnState(IntEnum):
|
||||||
|
TCP_SYN_SENT = 11 # SYN sent
|
||||||
|
TCP_ESTABLISHED = 12 # SYN+ACK received
|
||||||
|
TCP_FIN_WAIT_1 = 91 # FIN sent
|
||||||
|
TCP_CLOSE_WAIT = 92 # FIN received
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def can_timeout(state):
|
||||||
|
return state in (ConnState.TCP_SYN_SENT, ConnState.TCP_FIN_WAIT_1, ConnState.TCP_CLOSE_WAIT)
|
||||||
|
|
||||||
|
|
||||||
|
def repr_pkt(p):
|
||||||
|
try:
|
||||||
|
direction = p.direction.name
|
||||||
|
if p.is_loopback:
|
||||||
|
direction += "/lo"
|
||||||
|
except AttributeError: # windiver > 2.0
|
||||||
|
direction = 'OUT' if p.address.Outbound == 1 else 'IN'
|
||||||
|
if p.address.Loopback == 1:
|
||||||
|
direction += '/lo'
|
||||||
|
r = f"{direction} {p.src_addr}:{p.src_port}->{p.dst_addr}:{p.dst_port}"
|
||||||
|
if p.tcp:
|
||||||
|
t = p.tcp
|
||||||
|
r += f" {len(t.payload)}B ("
|
||||||
|
r += "+".join(
|
||||||
|
f.upper() for f in ("fin", "syn", "rst", "psh", "ack", "urg", "ece", "cwr", "ns") if getattr(t, f)
|
||||||
|
)
|
||||||
|
r += f") SEQ#{t.seq_num}"
|
||||||
|
if t.ack:
|
||||||
|
r += f" ACK#{t.ack_num}"
|
||||||
|
r += f" WZ={t.window_size}"
|
||||||
|
else:
|
||||||
|
r += f" {p.udp=} {p.icmpv4=} {p.icmpv6=}"
|
||||||
|
return f"<Pkt {r}>"
|
||||||
|
|
||||||
|
|
||||||
|
def synchronized_method(lock):
|
||||||
|
def decorator(method):
|
||||||
|
@wraps(method)
|
||||||
|
def wrapped(self, *args, **kwargs):
|
||||||
|
with getattr(self, lock):
|
||||||
|
return method(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class ConnTrack:
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if not cls._instance:
|
||||||
|
cls._instance = object.__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
raise RuntimeError("ConnTrack can not be instantiated multiple times")
|
||||||
|
|
||||||
|
def __init__(self, name, max_connections=0) -> None:
|
||||||
|
self.struct_full_tuple = Struct(">" + "".join(("B", "B", "16s", "H", "16s", "H", "L", "B")))
|
||||||
|
self.struct_src_tuple = Struct(">" + "".join(("B", "B", "16s", "H")))
|
||||||
|
self.struct_state_tuple = Struct(">" + "".join(("L", "B")))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.max_connections = max_connections
|
||||||
|
self.shm_list = shared_memory.ShareableList(
|
||||||
|
[bytes(self.struct_full_tuple.size) for _ in range(max_connections)], name=name
|
||||||
|
)
|
||||||
|
self.is_owner = True
|
||||||
|
self.next_slot = 0
|
||||||
|
self.used_slots = set()
|
||||||
|
self.rlock = threading.RLock()
|
||||||
|
except FileExistsError:
|
||||||
|
self.is_owner = False
|
||||||
|
self.shm_list = shared_memory.ShareableList(name=name)
|
||||||
|
self.max_connections = len(self.shm_list)
|
||||||
|
|
||||||
|
debug2(
|
||||||
|
f"ConnTrack: is_owner={self.is_owner} cap={len(self.shm_list)} item_sz={self.struct_full_tuple.size}B"
|
||||||
|
f"shm_name={self.shm_list.shm.name} shm_sz={self.shm_list.shm.size}B"
|
||||||
|
)
|
||||||
|
|
||||||
|
@synchronized_method("rlock")
|
||||||
|
def add(self, proto, src_addr, src_port, dst_addr, dst_port, state):
|
||||||
|
if not self.is_owner:
|
||||||
|
raise RuntimeError("Only owner can mutate ConnTrack")
|
||||||
|
if len(self.used_slots) >= self.max_connections:
|
||||||
|
raise RuntimeError(f"No slot available in ConnTrack {len(self.used_slots)}/{self.max_connections}")
|
||||||
|
|
||||||
|
if self.get(proto, src_addr, src_port):
|
||||||
|
return
|
||||||
|
|
||||||
|
for _ in range(self.max_connections):
|
||||||
|
if self.next_slot not in self.used_slots:
|
||||||
|
break
|
||||||
|
self.next_slot = (self.next_slot + 1) % self.max_connections
|
||||||
|
else:
|
||||||
|
raise RuntimeError("No slot available in ConnTrack") # should not be here
|
||||||
|
|
||||||
|
src_addr = ip_address(src_addr)
|
||||||
|
dst_addr = ip_address(dst_addr)
|
||||||
|
assert src_addr.version == dst_addr.version
|
||||||
|
ip_version = src_addr.version
|
||||||
|
state_epoch = int(time.time())
|
||||||
|
entry = (proto, ip_version, src_addr.packed, src_port, dst_addr.packed, dst_port, state_epoch, state)
|
||||||
|
packed = self.struct_full_tuple.pack(*entry)
|
||||||
|
self.shm_list[self.next_slot] = packed
|
||||||
|
self.used_slots.add(self.next_slot)
|
||||||
|
proto = IPProtocol(proto)
|
||||||
|
debug3(
|
||||||
|
f"ConnTrack: added ({proto.name} {src_addr}:{src_port}->{dst_addr}:{dst_port} @{state_epoch}:{state.name}) to "
|
||||||
|
f"slot={self.next_slot} | #ActiveConn={len(self.used_slots)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@synchronized_method("rlock")
|
||||||
|
def update(self, proto, src_addr, src_port, state):
|
||||||
|
if not self.is_owner:
|
||||||
|
raise RuntimeError("Only owner can mutate ConnTrack")
|
||||||
|
src_addr = ip_address(src_addr)
|
||||||
|
packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port)
|
||||||
|
for i in self.used_slots:
|
||||||
|
if self.shm_list[i].startswith(packed):
|
||||||
|
state_epoch = int(time.time())
|
||||||
|
self.shm_list[i] = self.shm_list[i][:-5] + self.struct_state_tuple.pack(state_epoch, state)
|
||||||
|
debug3(
|
||||||
|
f"ConnTrack: updated ({proto.name} {src_addr}:{src_port} @{state_epoch}:{state.name}) from slot={i} | "
|
||||||
|
f"#ActiveConn={len(self.used_slots)}"
|
||||||
|
)
|
||||||
|
return self._unpack(self.shm_list[i])
|
||||||
|
else:
|
||||||
|
debug3(
|
||||||
|
f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to update to {state.name} | "
|
||||||
|
f"#ActiveConn={len(self.used_slots)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@synchronized_method("rlock")
|
||||||
|
def remove(self, proto, src_addr, src_port):
|
||||||
|
if not self.is_owner:
|
||||||
|
raise RuntimeError("Only owner can mutate ConnTrack")
|
||||||
|
src_addr = ip_address(src_addr)
|
||||||
|
packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port)
|
||||||
|
for i in self.used_slots:
|
||||||
|
if self.shm_list[i].startswith(packed):
|
||||||
|
conn = self._unpack(self.shm_list[i])
|
||||||
|
self.shm_list[i] = b""
|
||||||
|
self.used_slots.remove(i)
|
||||||
|
debug3(
|
||||||
|
f"ConnTrack: removed ({proto.name} src={src_addr}:{src_port} state={conn.state.name}) from slot={i} | "
|
||||||
|
f"#ActiveConn={len(self.used_slots)}"
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
else:
|
||||||
|
debug3(
|
||||||
|
f"ConnTrack: ({proto.name} src={src_addr}:{src_port}) is not found to remove |"
|
||||||
|
f" #ActiveConn={len(self.used_slots)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, proto, src_addr, src_port):
|
||||||
|
src_addr = ip_address(src_addr)
|
||||||
|
packed = self.struct_src_tuple.pack(proto, src_addr.version, src_addr.packed, src_port)
|
||||||
|
for entry in self.shm_list:
|
||||||
|
if entry and entry.startswith(packed):
|
||||||
|
return self._unpack(entry)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
for entry in self.shm_list:
|
||||||
|
if not entry:
|
||||||
|
continue
|
||||||
|
conn = self._unpack(entry)
|
||||||
|
proto, ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, state = conn
|
||||||
|
log(f"{proto.name}/{ip_version} {src_addr}:{src_port} -> {dst_addr}:{dst_port} {state.name}@{state_epoch}")
|
||||||
|
|
||||||
|
@synchronized_method("rlock")
|
||||||
|
def gc(self, connection_timeout_sec=15):
|
||||||
|
# self.dump()
|
||||||
|
now = int(time.time())
|
||||||
|
n = 0
|
||||||
|
for i in tuple(self.used_slots):
|
||||||
|
state_packed = self.shm_list[i][-5:]
|
||||||
|
(state_epoch, state) = self.struct_state_tuple.unpack(state_packed)
|
||||||
|
if (now - state_epoch) < connection_timeout_sec:
|
||||||
|
continue
|
||||||
|
if ConnState.can_timeout(state):
|
||||||
|
conn = self._unpack(self.shm_list[i])
|
||||||
|
self.shm_list[i] = b""
|
||||||
|
self.used_slots.remove(i)
|
||||||
|
n += 1
|
||||||
|
debug3(
|
||||||
|
f"ConnTrack: GC: removed ({conn.protocol.name} src={conn.src_addr}:{conn.src_port} state={conn.state.name})"
|
||||||
|
f" from slot={i} | #ActiveConn={len(self.used_slots)}"
|
||||||
|
)
|
||||||
|
debug3(f"ConnTrack: GC: collected {n} connections | #ActiveConn={len(self.used_slots)}")
|
||||||
|
|
||||||
|
def _unpack(self, packed):
|
||||||
|
(
|
||||||
|
proto,
|
||||||
|
ip_version,
|
||||||
|
src_addr_packed,
|
||||||
|
src_port,
|
||||||
|
dst_addr_packed,
|
||||||
|
dst_port,
|
||||||
|
state_epoch,
|
||||||
|
state,
|
||||||
|
) = self.struct_full_tuple.unpack(packed)
|
||||||
|
dst_addr = ip_address(dst_addr_packed if ip_version == 6 else dst_addr_packed[:4]).exploded
|
||||||
|
src_addr = ip_address(src_addr_packed if ip_version == 6 else src_addr_packed[:4]).exploded
|
||||||
|
proto = IPProtocol(proto)
|
||||||
|
state = ConnState(state)
|
||||||
|
return ConnectionTuple(proto, ip_version, src_addr, src_port, dst_addr, dst_port, state_epoch, state)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
def conn_iter():
|
||||||
|
for i in self.used_slots:
|
||||||
|
yield self._unpack(self.shm_list[i])
|
||||||
|
|
||||||
|
return conn_iter()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ConnTrack(n={len(self.used_slots) if self.is_owner else '?'},cap={len(self.shm_list)},owner={self.is_owner})>"
|
||||||
|
|
||||||
|
|
||||||
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
network_config = {}
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
def _get_bind_address_for_port(self, port, family):
|
||||||
|
proto = "TCPv6" if family.version == 6 else "TCP"
|
||||||
|
for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode(errors='ignore').splitlines():
|
||||||
|
try:
|
||||||
|
_, local_addr, _, state, *_ = re.split(r"\s+", line.strip())
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
port_suffix = ":" + str(port)
|
||||||
|
if state == "LISTENING" and local_addr.endswith(port_suffix):
|
||||||
|
return ip_address(local_addr[:-len(port_suffix)].strip("[]"))
|
||||||
|
raise Fatal("Could not find listening address for {}/{}".format(port, proto))
|
||||||
|
|
||||||
|
def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user, group, tmark):
|
||||||
|
debug2(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {group=} {tmark=}")
|
||||||
|
|
||||||
|
if nslist or user or udp or group:
|
||||||
|
raise NotImplementedError("user, group, nslist, udp are not supported")
|
||||||
|
|
||||||
|
family = IPFamily(family)
|
||||||
|
|
||||||
|
proxy_ip = None
|
||||||
|
# using loopback only proxy binding won't work with windivert.
|
||||||
|
# See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 https://github.com/basil00/Divert/issues/82)
|
||||||
|
# As a workaround, finding another interface ip instead. (client should not bind proxy to loopback address)
|
||||||
|
proxy_bind_addr = self._get_bind_address_for_port(proxy_port, family)
|
||||||
|
if proxy_bind_addr.is_loopback:
|
||||||
|
raise Fatal("Windivert method requires proxy to be reachable by a non loopback address.")
|
||||||
|
if not proxy_bind_addr.is_unspecified:
|
||||||
|
proxy_ip = proxy_bind_addr
|
||||||
|
else:
|
||||||
|
local_addresses = [ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), 0, family=family)]
|
||||||
|
for addr in local_addresses:
|
||||||
|
if not addr.is_loopback and not addr.is_link_local:
|
||||||
|
proxy_ip = addr
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise Fatal("Windivert method requires proxy to be reachable by a non loopback address."
|
||||||
|
f"No address found for {family.name} in {local_addresses}")
|
||||||
|
debug2(f"Found non loopback address to connect to proxy: {proxy_ip}")
|
||||||
|
subnet_addresses = []
|
||||||
|
for (_, mask, exclude, network_addr, fport, lport) in subnets:
|
||||||
|
if fport and lport:
|
||||||
|
if lport > fport:
|
||||||
|
raise Fatal("lport must be less than or equal to fport")
|
||||||
|
ports = (fport, lport)
|
||||||
|
else:
|
||||||
|
ports = None
|
||||||
|
subnet_addresses.append((ip_network(f"{network_addr}/{mask}"), ports, exclude))
|
||||||
|
|
||||||
|
self.network_config[family] = {
|
||||||
|
"subnets": subnet_addresses,
|
||||||
|
"nslist": nslist,
|
||||||
|
"proxy_addr": (proxy_ip, proxy_port)
|
||||||
|
}
|
||||||
|
|
||||||
|
def wait_for_firewall_ready(self, sshuttle_pid):
|
||||||
|
debug2(f"network_config={self.network_config}")
|
||||||
|
self.conntrack = ConnTrack(f"sshuttle-windivert-{sshuttle_pid}", WINDIVERT_MAX_CONNECTIONS)
|
||||||
|
if not self.conntrack.is_owner:
|
||||||
|
raise Fatal("ConnTrack should be owner in wait_for_firewall_ready()")
|
||||||
|
thread_target_funcs = (self._egress_divert, self._ingress_divert, self._connection_gc)
|
||||||
|
ready_events = []
|
||||||
|
for fn in thread_target_funcs:
|
||||||
|
ev = threading.Event()
|
||||||
|
ready_events.append(ev)
|
||||||
|
|
||||||
|
def _target():
|
||||||
|
try:
|
||||||
|
fn(ev.set)
|
||||||
|
except Exception:
|
||||||
|
debug2(f"thread {fn.__name__} exiting due to: " + traceback.format_exc())
|
||||||
|
sys.stdin.close() # this will exist main thread
|
||||||
|
sys.stdout.close()
|
||||||
|
|
||||||
|
threading.Thread(name=fn.__name__, target=_target, daemon=True).start()
|
||||||
|
for ev in ready_events:
|
||||||
|
if not ev.wait(5): # at most 5 sec
|
||||||
|
raise Fatal("timeout in wait_for_firewall_ready()")
|
||||||
|
|
||||||
|
def restore_firewall(self, port, family, udp, user, group):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_supported_features(self):
|
||||||
|
result = super(Method, self).get_supported_features()
|
||||||
|
result.loopback_proxy_port = False
|
||||||
|
result.user = False
|
||||||
|
result.dns = False
|
||||||
|
# ipv6 only able to support with Windivert 2.x due to bugs in filter parsing
|
||||||
|
# TODO(nom3ad): Enable ipv6 once https://github.com/ffalcinelli/pydivert/pull/57 merged
|
||||||
|
result.ipv6 = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_tcp_dstip(self, sock):
|
||||||
|
if not hasattr(self, "conntrack"):
|
||||||
|
self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getpid()}")
|
||||||
|
if self.conntrack.is_owner:
|
||||||
|
raise Fatal("ConnTrack should not be owner in get_tcp_dstip()")
|
||||||
|
|
||||||
|
src_addr, src_port = sock.getpeername()
|
||||||
|
c = self.conntrack.get(IPProtocol.TCP, src_addr, src_port)
|
||||||
|
if not c:
|
||||||
|
return (src_addr, src_port)
|
||||||
|
return (c.dst_addr, c.dst_port)
|
||||||
|
|
||||||
|
def is_supported(self):
|
||||||
|
if sys.platform == "win32":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _egress_divert(self, ready_cb):
|
||||||
|
"""divert outgoing packets to proxy"""
|
||||||
|
proto = IPProtocol.TCP
|
||||||
|
filter = f"outbound and {proto.filter}"
|
||||||
|
af_filters = []
|
||||||
|
for af, c in self.network_config.items():
|
||||||
|
subnet_include_filters = []
|
||||||
|
subnet_exclude_filters = []
|
||||||
|
for ip_net, ports, exclude in c["subnets"]:
|
||||||
|
first_ip = ip_net.network_address.exploded
|
||||||
|
last_ip = ip_net.broadcast_address.exploded
|
||||||
|
if first_ip == last_ip:
|
||||||
|
_subnet_filter = f"{af.filter}.DstAddr=={first_ip}"
|
||||||
|
else:
|
||||||
|
_subnet_filter = f"{af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip}"
|
||||||
|
if ports:
|
||||||
|
if ports[0] == ports[1]:
|
||||||
|
_subnet_filter += f" and {proto.filter}.DstPort=={ports[0]}"
|
||||||
|
else:
|
||||||
|
_subnet_filter += f" and tcp.DstPort>={ports[0]} and tcp.DstPort<={ports[1]}"
|
||||||
|
(subnet_exclude_filters if exclude else subnet_include_filters).append(f"({_subnet_filter})")
|
||||||
|
_af_filter = f"{af.filter}"
|
||||||
|
if subnet_include_filters:
|
||||||
|
_af_filter += f" and ({' or '.join(subnet_include_filters)})"
|
||||||
|
if subnet_exclude_filters:
|
||||||
|
# TODO(noma3ad) use not() operator with Windivert2 after upgrade
|
||||||
|
_af_filter += f" and (({' or '.join(subnet_exclude_filters)})? false : true)"
|
||||||
|
proxy_ip, proxy_port = c["proxy_addr"]
|
||||||
|
# Avoids proxy outbound traffic getting directed to itself
|
||||||
|
proxy_guard_filter = f"(({af.filter}.DstAddr=={proxy_ip.exploded} and tcp.DstPort=={proxy_port})? false : true)"
|
||||||
|
_af_filter += f" and {proxy_guard_filter}"
|
||||||
|
af_filters.append(_af_filter)
|
||||||
|
if not af_filters:
|
||||||
|
raise Fatal("At least one ipv4 or ipv6 subnet is expected")
|
||||||
|
|
||||||
|
filter = f"{filter} and ({' or '.join(af_filters)})"
|
||||||
|
debug1(f"[EGRESS] {filter=}")
|
||||||
|
with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w:
|
||||||
|
proxy_ipv4, proxy_ipv6 = None, None
|
||||||
|
if IPFamily.IPv4 in self.network_config:
|
||||||
|
proxy_ipv4 = self.network_config[IPFamily.IPv4]["proxy_addr"]
|
||||||
|
proxy_ipv4 = proxy_ipv4[0].exploded, proxy_ipv4[1]
|
||||||
|
if IPFamily.IPv6 in self.network_config:
|
||||||
|
proxy_ipv6 = self.network_config[IPFamily.IPv6]["proxy_addr"]
|
||||||
|
proxy_ipv6 = proxy_ipv6[0].exploded, proxy_ipv6[1]
|
||||||
|
ready_cb()
|
||||||
|
verbose = get_verbose_level()
|
||||||
|
for pkt in w:
|
||||||
|
verbose >= 3 and debug3("[EGRESS] " + repr_pkt(pkt))
|
||||||
|
if pkt.tcp.syn and not pkt.tcp.ack:
|
||||||
|
# SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK)
|
||||||
|
self.conntrack.add(
|
||||||
|
socket.IPPROTO_TCP,
|
||||||
|
pkt.src_addr,
|
||||||
|
pkt.src_port,
|
||||||
|
pkt.dst_addr,
|
||||||
|
pkt.dst_port,
|
||||||
|
ConnState.TCP_SYN_SENT,
|
||||||
|
)
|
||||||
|
if pkt.tcp.fin:
|
||||||
|
# FIN sent (start of graceful close our side, and we wait for ACK)
|
||||||
|
self.conntrack.update(IPProtocol.TCP, pkt.src_addr, pkt.src_port, ConnState.TCP_FIN_WAIT_1)
|
||||||
|
if pkt.tcp.rst:
|
||||||
|
# RST sent (initiate abrupt connection teardown from our side, so we don't expect any reply)
|
||||||
|
self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port)
|
||||||
|
|
||||||
|
# DNAT
|
||||||
|
if pkt.ipv4 and proxy_ipv4:
|
||||||
|
pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv4
|
||||||
|
if pkt.ipv6 and proxy_ipv6:
|
||||||
|
pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv6
|
||||||
|
|
||||||
|
# XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well
|
||||||
|
# by setting src_addr to loopback address.
|
||||||
|
# Otherwise injecting packet will be ignored by Windows network stack
|
||||||
|
# as they packet has to cross public to private address space.
|
||||||
|
# See: https://github.com/basil00/Divert/issues/82
|
||||||
|
# Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets.
|
||||||
|
# >>> pkt.dst_addr = proxy_ipv4
|
||||||
|
w.send(pkt, recalculate_checksum=True)
|
||||||
|
|
||||||
|
def _ingress_divert(self, ready_cb):
|
||||||
|
"""handles incoming packets from proxy"""
|
||||||
|
proto = IPProtocol.TCP
|
||||||
|
# Windivert treats all local process traffic as outbound, regardless of origin external/loopback iface
|
||||||
|
direction = "outbound"
|
||||||
|
proxy_addr_filters = []
|
||||||
|
for af, c in self.network_config.items():
|
||||||
|
if not c["subnets"]:
|
||||||
|
continue
|
||||||
|
proxy_ip, proxy_port = c["proxy_addr"]
|
||||||
|
# "ip.SrcAddr=={hex(int(proxy_ip))}" # only Windivert >=2 supports this
|
||||||
|
proxy_addr_filters.append(f"{af.filter}.SrcAddr=={proxy_ip.exploded} and tcp.SrcPort=={proxy_port}")
|
||||||
|
if not proxy_addr_filters:
|
||||||
|
raise Fatal("At least one ipv4 or ipv6 address is expected")
|
||||||
|
filter = f"{direction} and {proto.filter} and ({' or '.join(proxy_addr_filters)})"
|
||||||
|
debug1(f"[INGRESS] {filter=}")
|
||||||
|
with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w:
|
||||||
|
ready_cb()
|
||||||
|
verbose = get_verbose_level()
|
||||||
|
for pkt in w:
|
||||||
|
verbose >= 3 and debug3("[INGRESS] " + repr_pkt(pkt))
|
||||||
|
if pkt.tcp.syn and pkt.tcp.ack:
|
||||||
|
# SYN+ACK received (connection established from proxy
|
||||||
|
conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED)
|
||||||
|
elif pkt.tcp.rst:
|
||||||
|
# RST received - Abrupt connection teardown initiated by proxy. Don't expect anymore packets
|
||||||
|
conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port)
|
||||||
|
# https://wiki.wireshark.org/TCP-4-times-close.md
|
||||||
|
elif pkt.tcp.fin and pkt.tcp.ack:
|
||||||
|
# FIN+ACK received (Passive close by proxy. Don't expect any more packets. proxy expects an ACK)
|
||||||
|
conn = self.conntrack.remove(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port)
|
||||||
|
elif pkt.tcp.fin:
|
||||||
|
# FIN received (proxy initiated graceful close. Expect a final ACK for a FIN packet)
|
||||||
|
conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_CLOSE_WAIT)
|
||||||
|
else:
|
||||||
|
# data fragments and ACKs
|
||||||
|
conn = self.conntrack.get(socket.IPPROTO_TCP, pkt.dst_addr, pkt.dst_port)
|
||||||
|
if not conn:
|
||||||
|
verbose >= 2 and debug2("Unexpected packet: " + repr_pkt(pkt))
|
||||||
|
continue
|
||||||
|
pkt.src_addr = conn.dst_addr
|
||||||
|
pkt.tcp.src_port = conn.dst_port
|
||||||
|
w.send(pkt, recalculate_checksum=True)
|
||||||
|
|
||||||
|
def _connection_gc(self, ready_cb):
|
||||||
|
ready_cb()
|
||||||
|
while True:
|
||||||
|
time.sleep(5)
|
||||||
|
self.conntrack.gc()
|
40
sshuttle/namespace.py
Normal file
40
sshuttle/namespace.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import os
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
|
||||||
|
from sshuttle.helpers import Fatal, debug1, debug2
|
||||||
|
|
||||||
|
|
||||||
|
CLONE_NEWNET = 0x40000000
|
||||||
|
NETNS_RUN_DIR = "/var/run/netns"
|
||||||
|
|
||||||
|
|
||||||
|
def enter_namespace(namespace, namespace_pid):
|
||||||
|
if namespace:
|
||||||
|
namespace_dir = f'{NETNS_RUN_DIR}/{namespace}'
|
||||||
|
else:
|
||||||
|
namespace_dir = f'/proc/{namespace_pid}/ns/net'
|
||||||
|
|
||||||
|
if not os.path.exists(namespace_dir):
|
||||||
|
raise Fatal('The namespace %r does not exists.' % namespace_dir)
|
||||||
|
|
||||||
|
debug2('loading libc')
|
||||||
|
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
|
||||||
|
|
||||||
|
default_errcheck = libc.setns.errcheck
|
||||||
|
|
||||||
|
def errcheck(ret, *args):
|
||||||
|
if ret == -1:
|
||||||
|
e = ctypes.get_errno()
|
||||||
|
raise Fatal(e, os.strerror(e))
|
||||||
|
if default_errcheck:
|
||||||
|
return default_errcheck(ret, *args)
|
||||||
|
|
||||||
|
libc.setns.errcheck = errcheck # type: ignore
|
||||||
|
|
||||||
|
debug1('Entering namespace %r' % namespace_dir)
|
||||||
|
|
||||||
|
with open(namespace_dir) as fd:
|
||||||
|
libc.setns(fd.fileno(), CLONE_NEWNET)
|
||||||
|
|
||||||
|
debug1('Namespace %r successfully set' % namespace_dir)
|
@ -1,5 +1,6 @@
|
|||||||
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__
|
||||||
@ -136,6 +137,15 @@ def parse_list(lst):
|
|||||||
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:
|
||||||
@ -234,9 +244,14 @@ parser.add_argument(
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
method_choices = ["auto", "windivert"]
|
||||||
|
else:
|
||||||
|
method_choices = ["auto", "nft", "nat", "tproxy", "pf", "ipfw"]
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--method",
|
"--method",
|
||||||
choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
|
choices=method_choices,
|
||||||
metavar="TYPE",
|
metavar="TYPE",
|
||||||
default="auto",
|
default="auto",
|
||||||
help="""
|
help="""
|
||||||
@ -301,6 +316,22 @@ 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]",
|
||||||
@ -438,3 +469,20 @@ parser.add_argument(
|
|||||||
hexadecimal (default '0x01')
|
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."""
|
||||||
|
)
|
||||||
|
@ -5,6 +5,7 @@ import traceback
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
import sshuttle.ssnet as ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
@ -13,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, \
|
||||||
resolvconf_random_nameserver, which, get_env
|
get_random_nameserver, which, get_env, SocketRWShim
|
||||||
|
|
||||||
|
|
||||||
def _ipmatch(ipstr):
|
def _ipmatch(ipstr):
|
||||||
@ -78,6 +79,20 @@ 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())
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
||||||
@ -100,14 +115,17 @@ def _list_routes(argv, extract_route):
|
|||||||
|
|
||||||
|
|
||||||
def list_routes():
|
def list_routes():
|
||||||
if which('ip'):
|
if sys.platform == 'win32':
|
||||||
routes = _list_routes(['ip', 'route'], _route_iproute)
|
routes = _list_routes(['route', 'PRINT', '-4'], _route_windows)
|
||||||
elif which('netstat'):
|
|
||||||
routes = _list_routes(['netstat', '-rn'], _route_netstat)
|
|
||||||
else:
|
else:
|
||||||
log('WARNING: Neither "ip" nor "netstat" were found on the server. '
|
if which('ip'):
|
||||||
'--auto-nets feature will not work.')
|
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.'):
|
||||||
@ -181,7 +199,7 @@ class DnsProxy(Handler):
|
|||||||
self.tries += 1
|
self.tries += 1
|
||||||
|
|
||||||
if self.to_nameserver is None:
|
if self.to_nameserver is None:
|
||||||
_, peer = resolvconf_random_nameserver(False)
|
_, peer = get_random_nameserver()
|
||||||
port = 53
|
port = 53
|
||||||
else:
|
else:
|
||||||
peer = self.to_ns_peer
|
peer = self.to_ns_peer
|
||||||
@ -281,7 +299,16 @@ def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
mux = Mux(sys.stdin, sys.stdout)
|
# get unbuffered stdin and stdout in binary mode. Equivalent to stdin.buffer/stdout.buffer (Only available in Python 3)
|
||||||
|
r, w = io.FileIO(0, mode='r'), io.FileIO(1, mode='w')
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
def _deferred_exit():
|
||||||
|
time.sleep(1) # give enough time to write logs to stderr
|
||||||
|
os._exit(23)
|
||||||
|
shim = SocketRWShim(r, w, on_end=_deferred_exit)
|
||||||
|
mux = Mux(*shim.makefiles())
|
||||||
|
else:
|
||||||
|
mux = Mux(r, w)
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
|
|
||||||
debug1('auto-nets:' + str(auto_nets))
|
debug1('auto-nets:' + str(auto_nets))
|
||||||
|
174
sshuttle/ssh.py
174
sshuttle/ssh.py
@ -12,7 +12,7 @@ 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, Fatal
|
from sshuttle.helpers import debug2, which, get_path, SocketRWShim, Fatal
|
||||||
|
|
||||||
|
|
||||||
def get_module_source(name):
|
def get_module_source(name):
|
||||||
@ -56,7 +56,7 @@ def parse_hostport(rhostport):
|
|||||||
# Fix #410 bad username error detect
|
# Fix #410 bad username error detect
|
||||||
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(":")
|
username, password = username.split(":", 1)
|
||||||
|
|
||||||
if ":" in host:
|
if ":" in host:
|
||||||
# IPv6 address and/or got a port specified
|
# IPv6 address and/or got a port specified
|
||||||
@ -84,7 +84,7 @@ def parse_hostport(rhostport):
|
|||||||
return username, password, port, host
|
return username, password, port, host
|
||||||
|
|
||||||
|
|
||||||
def connect(ssh_cmd, rhostport, python, stderr, options):
|
def connect(ssh_cmd, rhostport, python, stderr, add_cmd_delimiter, remote_shell, options):
|
||||||
username, password, port, host = parse_hostport(rhostport)
|
username, password, port, host = parse_hostport(rhostport)
|
||||||
if username:
|
if username:
|
||||||
rhost = "{}@{}".format(username, host)
|
rhost = "{}@{}".format(username, host)
|
||||||
@ -115,8 +115,8 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
pyscript = r"""
|
pyscript = r"""
|
||||||
import sys, os;
|
import sys, os;
|
||||||
verbosity=%d;
|
verbosity=%d;
|
||||||
sys.stdin = os.fdopen(0, "rb");
|
stdin = os.fdopen(0, 'rb');
|
||||||
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"));
|
exec(compile(stdin.read(%d), 'assembler.py', 'exec'));
|
||||||
sys.exit(98);
|
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())
|
||||||
@ -134,62 +134,72 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
portl = ["-p", str(port)]
|
portl = ["-p", str(port)]
|
||||||
else:
|
else:
|
||||||
portl = []
|
portl = []
|
||||||
if python:
|
if remote_shell == "cmd":
|
||||||
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
pycmd = '"%s" -c "%s"' % (python or 'python', pyscript)
|
||||||
else:
|
elif remote_shell == "powershell":
|
||||||
# By default, we run the following code in a shell.
|
for c in ('\'', ' ', ';', '(', ')', ','):
|
||||||
# However, with restricted shells and other unusual
|
pyscript = pyscript.replace(c, '`' + c)
|
||||||
# situations, there can be trouble. See the RESTRICTED
|
pycmd = '%s -c %s' % (python or 'python', pyscript)
|
||||||
# SHELL section in "man bash" for more information. The
|
else: # posix shell expected
|
||||||
# code makes many assumptions:
|
if python:
|
||||||
#
|
pycmd = '"%s" -c "%s"' % (python, pyscript)
|
||||||
# (1) That /bin/sh exists and that we can call it.
|
else:
|
||||||
# Restricted shells often do *not* allow you to run
|
# By default, we run the following code in a shell.
|
||||||
# programs specified with an absolute path like /bin/sh.
|
# However, with restricted shells and other unusual
|
||||||
# Either way, if there is trouble with this, it should
|
# situations, there can be trouble. See the RESTRICTED
|
||||||
# return error code 127.
|
# SHELL section in "man bash" for more information. The
|
||||||
#
|
# code makes many assumptions:
|
||||||
# (2) python3 or python exists in the PATH and is
|
#
|
||||||
# executable. If they aren't, then exec won't work (see (4)
|
# (1) That /bin/sh exists and that we can call it.
|
||||||
# below).
|
# Restricted shells often do *not* allow you to run
|
||||||
#
|
# programs specified with an absolute path like /bin/sh.
|
||||||
# (3) In /bin/sh, that we can redirect stderr in order to
|
# Either way, if there is trouble with this, it should
|
||||||
# hide the version that "python3 -V" might print (some
|
# return error code 127.
|
||||||
# restricted shells don't allow redirection, see
|
#
|
||||||
# RESTRICTED SHELL section in 'man bash'). However, if we
|
# (2) python3 or python exists in the PATH and is
|
||||||
# are in a restricted shell, we'd likely have trouble with
|
# executable. If they aren't, then exec won't work (see (4)
|
||||||
# assumption (1) above.
|
# below).
|
||||||
#
|
#
|
||||||
# (4) The 'exec' command should work except if we failed
|
# (3) In /bin/sh, that we can redirect stderr in order to
|
||||||
# to exec python because it doesn't exist or isn't
|
# hide the version that "python3 -V" might print (some
|
||||||
# executable OR if exec isn't allowed (some restricted
|
# restricted shells don't allow redirection, see
|
||||||
# shells don't allow exec). If the exec succeeded, it will
|
# RESTRICTED SHELL section in 'man bash'). However, if we
|
||||||
# not return and not get to the "exit 97" command. If exec
|
# are in a restricted shell, we'd likely have trouble with
|
||||||
# does return, we exit with code 97.
|
# assumption (1) above.
|
||||||
#
|
#
|
||||||
# Specifying the exact python program to run with --python
|
# (4) The 'exec' command should work except if we failed
|
||||||
# avoids many of the issues above. However, if
|
# to exec python because it doesn't exist or isn't
|
||||||
# you have a restricted shell on remote, you may only be
|
# executable OR if exec isn't allowed (some restricted
|
||||||
# able to run python if it is in your PATH (and you can't
|
# shells don't allow exec). If the exec succeeded, it will
|
||||||
# run programs specified with an absolute path). In that
|
# not return and not get to the "exit 97" command. If exec
|
||||||
# case, sshuttle might not work at all since it is not
|
# does return, we exit with code 97.
|
||||||
# possible to run python on the remote machine---even if
|
#
|
||||||
# it is present.
|
# Specifying the exact python program to run with --python
|
||||||
pycmd = ("P=python3; $P -V 2>%s || P=python; "
|
# avoids many of the issues above. However, if
|
||||||
"exec \"$P\" -c %s; exit 97") % \
|
# you have a restricted shell on remote, you may only be
|
||||||
(os.devnull, quote(pyscript))
|
# able to run python if it is in your PATH (and you can't
|
||||||
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
|
# 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 +
|
portl + [rhost])
|
||||||
[rhost, '--', pycmd])
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
argv = (sshl +
|
argv = (sshl + portl + [rhost])
|
||||||
portl +
|
|
||||||
[rhost, '--', pycmd])
|
if add_cmd_delimiter:
|
||||||
|
argv += ['--', pycmd]
|
||||||
|
else:
|
||||||
|
argv += [pycmd]
|
||||||
|
|
||||||
# Our which() function searches for programs in get_path()
|
# Our which() function searches for programs in get_path()
|
||||||
# directories (which include PATH). This step isn't strictly
|
# directories (which include PATH). This step isn't strictly
|
||||||
@ -201,19 +211,45 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path()))
|
raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path()))
|
||||||
argv[0] = abs_path
|
argv[0] = abs_path
|
||||||
|
|
||||||
(s1, s2) = socket.socketpair()
|
if sys.platform != 'win32':
|
||||||
|
(s1, s2) = socket.socketpair()
|
||||||
|
pstdin, pstdout = os.dup(s1.fileno()), os.dup(s1.fileno())
|
||||||
|
|
||||||
def setup():
|
def preexec_fn():
|
||||||
# runs in the child process
|
# runs in the child process
|
||||||
s2.close()
|
s2.close()
|
||||||
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
|
s1.close()
|
||||||
s1.close()
|
|
||||||
|
|
||||||
debug2('executing: %r' % argv)
|
def get_server_io():
|
||||||
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
os.close(pstdin)
|
||||||
close_fds=True, stderr=stderr)
|
os.close(pstdout)
|
||||||
os.close(s1a)
|
return s2.makefile("rb", buffering=0), s2.makefile("wb", buffering=0)
|
||||||
os.close(s1b)
|
else:
|
||||||
s2.sendall(content)
|
# In Windows CPython, BSD sockets are not supported as subprocess stdio
|
||||||
s2.sendall(content2)
|
# and select.select() used in ssnet.py won't work on Windows pipes.
|
||||||
return p, s2
|
# 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
|
||||||
|
@ -4,9 +4,8 @@ import socket
|
|||||||
import errno
|
import errno
|
||||||
import select
|
import select
|
||||||
import os
|
import os
|
||||||
import fcntl
|
|
||||||
|
|
||||||
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
|
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, set_non_blocking_io
|
||||||
|
|
||||||
MAX_CHANNEL = 65535
|
MAX_CHANNEL = 65535
|
||||||
LATENCY_BUFFER_SIZE = 32768
|
LATENCY_BUFFER_SIZE = 32768
|
||||||
@ -78,7 +77,8 @@ def _fds(socks):
|
|||||||
def _nb_clean(func, *args):
|
def _nb_clean(func, *args):
|
||||||
try:
|
try:
|
||||||
return func(*args)
|
return func(*args)
|
||||||
except OSError:
|
except (OSError, socket.error):
|
||||||
|
# Note: In python2 socket.error != OSError (In python3, they are same)
|
||||||
_, e = sys.exc_info()[:2]
|
_, 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
|
||||||
@ -168,19 +168,25 @@ class SockWrapper:
|
|||||||
debug3('%r: fixed connect result: %s' % (self, e))
|
debug3('%r: fixed connect result: %s' % (self, e))
|
||||||
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
||||||
pass # not connected yet
|
pass # not connected yet
|
||||||
|
elif sys.platform == 'win32' and e.args[0] == errno.WSAEWOULDBLOCK: # 10035
|
||||||
|
pass # not connected yet
|
||||||
elif e.args[0] == 0:
|
elif e.args[0] == 0:
|
||||||
# connected successfully (weird Linux bug?)
|
if sys.platform == 'win32':
|
||||||
# Sometimes Linux seems to return EINVAL when it isn't
|
# On Windows "real" error of EINVAL could be 0, when socket is in connecting state
|
||||||
# invalid. This *may* be caused by a race condition
|
pass
|
||||||
# between connect() and getsockopt(SO_ERROR) (ie. it
|
else:
|
||||||
# finishes connecting in between the two, so there is no
|
# connected successfully (weird Linux bug?)
|
||||||
# longer an error). However, I'm not sure of that.
|
# Sometimes Linux seems to return EINVAL when it isn't
|
||||||
#
|
# invalid. This *may* be caused by a race condition
|
||||||
# I did get at least one report that the problem went away
|
# between connect() and getsockopt(SO_ERROR) (ie. it
|
||||||
# when we added this, however.
|
# finishes connecting in between the two, so there is no
|
||||||
self.connect_to = None
|
# longer an error). However, I'm not sure of that.
|
||||||
|
#
|
||||||
|
# I did get at least one report that the problem went away
|
||||||
|
# when we added this, however.
|
||||||
|
self.connect_to = None
|
||||||
elif e.args[0] == errno.EISCONN:
|
elif e.args[0] == errno.EISCONN:
|
||||||
# connected successfully (BSD)
|
# connected successfully (BSD + Windows)
|
||||||
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
|
||||||
@ -213,7 +219,7 @@ class SockWrapper:
|
|||||||
return 0 # still connecting
|
return 0 # still connecting
|
||||||
self.wsock.setblocking(False)
|
self.wsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
return _nb_clean(self.wsock.send, 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:
|
||||||
@ -236,7 +242,7 @@ class SockWrapper:
|
|||||||
return
|
return
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
return _nb_clean(self.rsock.recv, 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)
|
||||||
@ -382,11 +388,13 @@ class Mux(Handler):
|
|||||||
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
|
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
||||||
len(data), self.fullness))
|
len(data), self.fullness))
|
||||||
|
# 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'
|
||||||
% (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:
|
||||||
@ -431,15 +439,10 @@ class Mux(Handler):
|
|||||||
callback(cmd, data)
|
callback(cmd, data)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
try:
|
set_non_blocking_io(self.wfile.fileno())
|
||||||
os.set_blocking(self.wfile.fileno(), False)
|
|
||||||
except AttributeError:
|
|
||||||
# python < 3.5
|
|
||||||
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL)
|
|
||||||
flags |= os.O_NONBLOCK
|
|
||||||
fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
|
|
||||||
if self.outbuf and self.outbuf[0]:
|
if self.outbuf and self.outbuf[0]:
|
||||||
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
|
wrote = _nb_clean(self.wfile.write, self.outbuf[0])
|
||||||
|
# self.wfile.flush()
|
||||||
debug2('mux wrote: %r/%d' % (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:]
|
||||||
@ -447,18 +450,12 @@ class Mux(Handler):
|
|||||||
self.outbuf[0:1] = []
|
self.outbuf[0:1] = []
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
try:
|
set_non_blocking_io(self.rfile.fileno())
|
||||||
os.set_blocking(self.rfile.fileno(), False)
|
|
||||||
except AttributeError:
|
|
||||||
# python < 3.5
|
|
||||||
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL)
|
|
||||||
flags |= os.O_NONBLOCK
|
|
||||||
fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
|
|
||||||
try:
|
try:
|
||||||
# If LATENCY_BUFFER_SIZE is inappropriately large, we will
|
# If LATENCY_BUFFER_SIZE is inappropriately large, we will
|
||||||
# get a MemoryError here. Read no more than 1MiB.
|
# get a MemoryError here. Read no more than 1MiB.
|
||||||
read = _nb_clean(os.read, self.rfile.fileno(),
|
read = _nb_clean(self.rfile.read, min(1048576, LATENCY_BUFFER_SIZE))
|
||||||
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)
|
||||||
|
@ -5,7 +5,15 @@ from uuid import uuid4
|
|||||||
|
|
||||||
|
|
||||||
def build_config(user_name):
|
def build_config(user_name):
|
||||||
template = '''
|
"""Generates a sudoers configuration to allow passwordless execution of sshuttle."""
|
||||||
|
|
||||||
|
argv0 = os.path.abspath(sys.argv[0])
|
||||||
|
is_python_script = argv0.endswith('.py')
|
||||||
|
executable = f"{sys.executable} {argv0}" if is_python_script else argv0
|
||||||
|
dist_packages = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
cmd_alias = f"SSHUTTLE{uuid4().hex[-3:].upper()}"
|
||||||
|
|
||||||
|
template = f"""
|
||||||
# WARNING: If you intend to restrict a user to only running the
|
# WARNING: If you intend to restrict a user to only running the
|
||||||
# sshuttle command as root, THIS CONFIGURATION IS INSECURE.
|
# sshuttle command as root, THIS CONFIGURATION IS INSECURE.
|
||||||
# When a user can run sshuttle as root (with or without a password),
|
# When a user can run sshuttle as root (with or without a password),
|
||||||
@ -16,27 +24,18 @@ def build_config(user_name):
|
|||||||
# sshuttle without needing to enter a sudo password. To use this
|
# sshuttle without needing to enter a sudo password. To use this
|
||||||
# configuration, run 'visudo /etc/sudoers.d/sshuttle_auto' as root and
|
# 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
|
# paste this text into the editor that it opens. If you want to give
|
||||||
# multiple users these privileges, you may wish to use use different
|
# multiple users these privileges, you may wish to use different
|
||||||
# filenames for each one (i.e., /etc/sudoers.d/sshuttle_auto_john).
|
# filenames for each one (i.e., /etc/sudoers.d/sshuttle_auto_john).
|
||||||
|
|
||||||
# This configuration was initially generated by the
|
# This configuration was initially generated by the
|
||||||
# 'sshuttle --sudoers-no-modify' command.
|
# 'sshuttle --sudoers-no-modify' command.
|
||||||
|
|
||||||
Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
|
Cmnd_Alias {cmd_alias} = /usr/bin/env PYTHONPATH={dist_packages} {executable} *
|
||||||
|
|
||||||
%(user_name)s ALL=NOPASSWD: %(ca)s
|
{user_name} ALL=NOPASSWD: {cmd_alias}
|
||||||
'''
|
"""
|
||||||
|
|
||||||
content = template % {
|
return template
|
||||||
# randomize command alias to avoid collisions
|
|
||||||
'ca': 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()},
|
|
||||||
'dist_packages': os.path.dirname(os.path.abspath(__file__))[:-9],
|
|
||||||
'py': sys.executable,
|
|
||||||
'path': sys.argv[0],
|
|
||||||
'user_name': user_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def sudoers(user_name=None):
|
def sudoers(user_name=None):
|
||||||
|
@ -1 +0,0 @@
|
|||||||
__version__ = version = '1.1.2'
|
|
@ -10,7 +10,7 @@ import sshuttle.firewall
|
|||||||
|
|
||||||
|
|
||||||
def setup_daemon():
|
def setup_daemon():
|
||||||
stdin = io.StringIO(u"""ROUTES
|
stdin = io.BytesIO(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
|
||||||
@ -21,7 +21,7 @@ NSLIST
|
|||||||
PORTS 1024,1025,1026,1027
|
PORTS 1024,1025,1026,1027
|
||||||
GO 1 - - 0x01 12345
|
GO 1 - - 0x01 12345
|
||||||
HOST 1.2.3.3,existing
|
HOST 1.2.3.3,existing
|
||||||
""".format(inet=AF_INET, inet6=AF_INET6))
|
""".format(inet=AF_INET, inet6=AF_INET6).encode('ASCII'))
|
||||||
stdout = Mock()
|
stdout = Mock()
|
||||||
return stdin, stdout
|
return stdin, stdout
|
||||||
|
|
||||||
@ -127,9 +127,9 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
|
|||||||
]
|
]
|
||||||
|
|
||||||
assert stdout.mock_calls == [
|
assert stdout.mock_calls == [
|
||||||
call.write('READY test\n'),
|
call.write(b'READY test\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('STARTED\n'),
|
call.write(b'STARTED\n'),
|
||||||
call.flush()
|
call.flush()
|
||||||
]
|
]
|
||||||
assert mock_setup_daemon.mock_calls == [call()]
|
assert mock_setup_daemon.mock_calls == [call()]
|
||||||
@ -157,6 +157,7 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
'0x01'),
|
'0x01'),
|
||||||
|
call().wait_for_firewall_ready(12345),
|
||||||
call().restore_firewall(1024, AF_INET6, True, None, None),
|
call().restore_firewall(1024, AF_INET6, True, None, None),
|
||||||
call().restore_firewall(1025, AF_INET, True, None, None),
|
call().restore_firewall(1025, AF_INET, True, None, None),
|
||||||
]
|
]
|
||||||
|
@ -143,7 +143,7 @@ nameserver 2404:6800:4004:80c::4
|
|||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.helpers.open', create=True)
|
@patch('sshuttle.helpers.open', create=True)
|
||||||
def test_resolvconf_random_nameserver(mock_open):
|
def test_get_random_nameserver(mock_open):
|
||||||
mock_open.return_value = io.StringIO(u"""
|
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.resolvconf_random_nameserver(False)
|
ns = sshuttle.helpers.get_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'),
|
||||||
|
@ -418,8 +418,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
'0x01')
|
'0x01')
|
||||||
|
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
||||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
||||||
]
|
]
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-s Interfaces -i lo -v'),
|
call('-s Interfaces -i lo -v'),
|
||||||
@ -470,8 +470,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
None,
|
None,
|
||||||
'0x01')
|
'0x01')
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
||||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
call(mock_pf_get_dev(), 0xcd50441a, ANY),
|
||||||
]
|
]
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-s Interfaces -i lo -v'),
|
call('-s Interfaces -i lo -v'),
|
||||||
|
@ -176,3 +176,33 @@ def test_parse_subnetport_host_with_port(mock_getaddrinfo):
|
|||||||
(socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 90),
|
(socket.AF_INET6, '2404:6800:4004:821::2001', 128, 80, 90),
|
||||||
(socket.AF_INET, '142.251.42.129', 32, 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)
|
||||||
|
3
tox.ini
3
tox.ini
@ -7,9 +7,10 @@ envlist =
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
basepython =
|
basepython =
|
||||||
py38: python3.8
|
|
||||||
py39: python3.9
|
py39: python3.9
|
||||||
py310: python3.10
|
py310: python3.10
|
||||||
|
py311: python3.11
|
||||||
|
py312: python3.12
|
||||||
commands =
|
commands =
|
||||||
pip install -e .
|
pip install -e .
|
||||||
# actual flake8 test
|
# actual flake8 test
|
||||||
|
Reference in New Issue
Block a user