mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 08:40:30 +02:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
99c4abce81 | |||
a2d405a6a7 | |||
7fa927ef8c | |||
a1dd6859b0 | |||
8a123d9762 | |||
cbe3d1e402 | |||
340ccc705e | |||
1f5e6cea70 | |||
fd6b6bb71f | |||
5b08caaeb1 | |||
40f6c1d4f2 |
45
.github/workflows/release-please.yml
vendored
45
.github/workflows/release-please.yml
vendored
@ -3,28 +3,53 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
name: release-please
|
||||
|
||||
jobs:
|
||||
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
|
||||
release-type: python
|
||||
|
||||
upload-pypi:
|
||||
name: Upload to pypi
|
||||
needs: [release-please]
|
||||
if: ${{ needs.release_please.outputs.release_created == 'true' }}
|
||||
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/sshuttle
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Publish PyPi package
|
||||
uses: code-specialist/pypi-poetry-publish@v1
|
||||
with:
|
||||
ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PUBLISH_REGISTRY_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: Run image
|
||||
uses: abatilo/actions-poetry@v4
|
||||
with:
|
||||
poetry-version: main
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
- uses: actions/cache@v4
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
- name: Install the project dependencies
|
||||
run: poetry install
|
||||
- name: Package project
|
||||
run: poetry build
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
20
CHANGELOG.md
20
CHANGELOG.md
@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
83
README.rst
83
README.rst
@ -30,88 +30,9 @@ common case:
|
||||
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
|
||||
|
||||
- Windows::
|
||||
Use PyPI
|
||||
|
||||
pip install sshuttle
|
||||
.. _Documentation: https://sshuttle.readthedocs.io/en/stable/installation.html
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
@ -1,23 +1,84 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
- Ubuntu 16.04 or later::
|
||||
|
||||
apt-get install sshuttle
|
||||
|
||||
- Debian stretch or later::
|
||||
|
||||
apt-get install sshuttle
|
||||
|
||||
- Arch Linux::
|
||||
|
||||
pacman -S sshuttle
|
||||
|
||||
- Fedora::
|
||||
|
||||
dnf install sshuttle
|
||||
|
||||
- openSUSE::
|
||||
|
||||
zypper in sshuttle
|
||||
|
||||
- Gentoo::
|
||||
|
||||
emerge -av net-proxy/sshuttle
|
||||
|
||||
- NixOS::
|
||||
|
||||
nix-env -iA nixos.sshuttle
|
||||
|
||||
- From PyPI::
|
||||
|
||||
pip install sshuttle
|
||||
|
||||
- Debian package manager::
|
||||
|
||||
sudo apt install sshuttle
|
||||
sudo pip install sshuttle
|
||||
|
||||
- Clone::
|
||||
|
||||
git clone https://github.com/sshuttle/sshuttle.git
|
||||
cd sshuttle
|
||||
./setup.py install
|
||||
sudo ./setup.py install
|
||||
|
||||
- FreeBSD::
|
||||
|
||||
Optionally after installation
|
||||
-----------------------------
|
||||
# ports
|
||||
cd /usr/ports/net/py-sshuttle && make install clean
|
||||
# pkg
|
||||
pkg install py39-sshuttle
|
||||
|
||||
- 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
|
||||
|
8
poetry.lock
generated
8
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alabaster"
|
||||
@ -428,13 +428,13 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "7.1.1"
|
||||
version = "7.1.2"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
optional = false
|
||||
python-versions = ">=3.8.1"
|
||||
files = [
|
||||
{file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"},
|
||||
{file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"},
|
||||
{file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"},
|
||||
{file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "sshuttle"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
description = "Transparent proxy server that works as a poor man's VPN. Forwards over ssh. Doesn't require admin. Works with Linux and MacOS. Supports DNS tunneling."
|
||||
authors = ["Brian May <brian@linuxpenguins.xyz>"]
|
||||
license = "LGPL-2.1"
|
||||
|
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 1.2.0
|
||||
current_version = 1.3.0
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
|
@ -11,6 +11,7 @@ import sshuttle.ssyslog as ssyslog
|
||||
from sshuttle.options import parser, parse_ipport
|
||||
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
||||
from sshuttle.sudoers import sudoers
|
||||
from sshuttle.namespace import enter_namespace
|
||||
|
||||
|
||||
def main():
|
||||
@ -37,6 +38,16 @@ def main():
|
||||
helpers.verbose = opt.verbose
|
||||
|
||||
try:
|
||||
# Since namespace and namespace-pid options are only available
|
||||
# in linux, we must check if it exists with getattr
|
||||
namespace = getattr(opt, 'namespace', None)
|
||||
namespace_pid = getattr(opt, 'namespace_pid', None)
|
||||
if namespace or namespace_pid:
|
||||
prefix = helpers.logprefix
|
||||
helpers.logprefix = 'ns: '
|
||||
enter_namespace(namespace, namespace_pid)
|
||||
helpers.logprefix = prefix
|
||||
|
||||
if opt.firewall:
|
||||
if opt.subnets or opt.subnets_file:
|
||||
parser.error('exactly zero arguments expected')
|
||||
|
@ -20,7 +20,7 @@ def ipt_chain_exists(family, table, name):
|
||||
argv = [cmd, '-w', '-t', table, '-nL']
|
||||
try:
|
||||
output = ssubprocess.check_output(argv, env=get_env())
|
||||
for line in output.decode('ASCII').split('\n'):
|
||||
for line in output.decode('ASCII', errors='replace').split('\n'):
|
||||
if line.startswith('Chain %s ' % name):
|
||||
return True
|
||||
except ssubprocess.CalledProcessError as e:
|
||||
|
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)
|
@ -137,6 +137,15 @@ def parse_list(lst):
|
||||
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
||||
|
||||
|
||||
def parse_namespace(namespace):
|
||||
try:
|
||||
assert re.fullmatch(
|
||||
r'(@?[a-z_A-Z]\w+(?:\.@?[a-z_A-Z]\w+)*)', namespace)
|
||||
return namespace
|
||||
except AssertionError:
|
||||
raise Fatal("%r is not a valid namespace name." % namespace)
|
||||
|
||||
|
||||
class Concat(Action):
|
||||
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||
if nargs is not None:
|
||||
@ -460,3 +469,20 @@ parser.add_argument(
|
||||
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."""
|
||||
)
|
||||
|
@ -56,7 +56,7 @@ def parse_hostport(rhostport):
|
||||
# Fix #410 bad username error detect
|
||||
if ":" in username:
|
||||
# this will even allow for the username to be empty
|
||||
username, password = username.split(":")
|
||||
username, password = username.split(":", 1)
|
||||
|
||||
if ":" in host:
|
||||
# IPv6 address and/or got a port specified
|
||||
|
@ -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_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)
|
||||
|
Reference in New Issue
Block a user