mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 08:40:30 +02:00
Compare commits
93 Commits
Author | SHA1 | Date | |
---|---|---|---|
8c9dad1c6b | |||
7fde5b6fa6 | |||
734168531f | |||
d058d9bc93 | |||
1db3281c16 | |||
bef54e778d | |||
9bcca27965 | |||
d0f0aa9f17 | |||
ec2018d664 | |||
c9bc389992 | |||
9f27c1943b | |||
6c21addde9 | |||
4b320180c4 | |||
994ce07466 | |||
34197c492c | |||
75eaac7e06 | |||
b0c87b01b7 | |||
cf32a5cfa8 | |||
f674aacdc8 | |||
432f86f253 | |||
b63e58f494 | |||
88ce5c0bca | |||
50a4c36635 | |||
25461c70a3 | |||
365d30de14 | |||
6e19496fb7 | |||
534ad8dfed | |||
535eb62928 | |||
966fd0c523 | |||
580462156e | |||
9e78abd2c2 | |||
e3201969b5 | |||
9b10df21b6 | |||
accb4ecc97 | |||
ee36cc0d00 | |||
f0c13eafe9 | |||
36d34b18da | |||
3edeb726b8 | |||
6e9c58b4b4 | |||
13db89916a | |||
84076f29fa | |||
ad31ac4e18 | |||
69d3f7dc64 | |||
6ad4473c87 | |||
23516ebd71 | |||
c69b9d6f4b | |||
2d92090625 | |||
502b36e990 | |||
fe1df00be7 | |||
a32689d030 | |||
a7193f508a | |||
7ebff92637 | |||
138d2877c6 | |||
21ef365c59 | |||
a765aa3235 | |||
71f2248b07 | |||
935393b261 | |||
3e2ad68796 | |||
635cf8605e | |||
cb917d7e6c | |||
4372c6c117 | |||
4e945ca4de | |||
3bfb975ed9 | |||
752a953101 | |||
61f4cd9de5 | |||
8e35f049e2 | |||
0e99adc5d1 | |||
04849df7e3 | |||
531a17c151 | |||
d43db80dec | |||
0b1a260436 | |||
efc854c33e | |||
ca41026c89 | |||
b473b91633 | |||
7a54d12f80 | |||
d4bbf3b68d | |||
41f5b3e9c1 | |||
c780597de3 | |||
d085a419b2 | |||
842768f9cf | |||
97ed2030f3 | |||
6dc368bde8 | |||
f528bb9846 | |||
561b648e4b | |||
0dba8a8beb | |||
7b6f082454 | |||
1ec17e1b1b | |||
cecccc2efd | |||
db69ba6d8d | |||
2bb92cd6d4 | |||
ae5bd28dcf | |||
55bd78fd43 | |||
1f5ed9c66e |
35
.github/workflows/pythonpackage.yml
vendored
Normal file
35
.github/workflows/pythonpackage.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-tests.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
flake8 sshuttle tests --count --show-source --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
PYTHONPATH=$PWD pytest
|
19
.travis.yml
19
.travis.yml
@ -1,19 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- pypy
|
||||
|
||||
install:
|
||||
- travis_retry pip install -q -r requirements-tests.txt
|
||||
|
||||
before_script:
|
||||
# stop the build if there are Python syntax errors or undefined names.
|
||||
- flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide.
|
||||
- flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
script:
|
||||
- PYTHONPATH=. py.test
|
80
CHANGES.rst
80
CHANGES.rst
@ -9,6 +9,86 @@ adheres to `Semantic Versioning`_.
|
||||
.. _`Semantic Versioning`: http://semver.org/
|
||||
|
||||
|
||||
1.0.1 - 2020-06-05
|
||||
------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Errors in python long_documentation.
|
||||
|
||||
|
||||
1.0.0 - 2020-06-05
|
||||
------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
* Python 3.8 support.
|
||||
* sshpass support.
|
||||
* Auto sudoers file (#269).
|
||||
* option for latency control buffer size.
|
||||
* Docs: FreeBSD'.
|
||||
* Docs: Nix'.
|
||||
* Docs: openwrt'.
|
||||
* Docs: install instructions for Fedora'.
|
||||
* Docs: install instructions for Arch Linux'.
|
||||
* Docs: 'My VPN broke and need a solution fast'.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
* Python 2.6 support.
|
||||
* Python 2.7 support.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Remove debug message for getpeername failure.
|
||||
* Fix crash triggered by port scans closing socket.
|
||||
* Added "Running as a service" to docs.
|
||||
* Systemd integration.
|
||||
* Trap UnicodeError to handle cases where hostnames returned by DNS are invalid.
|
||||
* Formatting error in CHANGES.rst
|
||||
* Various errors in documentation.
|
||||
* Nftables based method.
|
||||
* Make hostwatch locale-independent (#379).
|
||||
* Add tproxy udp port mark filter that was missed in #144, fixes #367.
|
||||
* Capturing of local DNS servers.
|
||||
* Crashing on ECONNABORTED.
|
||||
* Size of pf_rule, which grew in OpenBSD 6.4.
|
||||
* Use prompt for sudo, not needed for doas.
|
||||
* Arch linux installation instructions.
|
||||
* tests for existing PR-312 (#337).
|
||||
* Hyphen in hostname.
|
||||
* Assembler import (#319).
|
||||
|
||||
|
||||
0.78.5 - 2019-01-28
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
* doas support as replacmeent for sudo on OpenBSD.
|
||||
* Added ChromeOS section to documentation (#262)
|
||||
* Add --no-sudo-pythonpath option
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Fix forwarding to a single port.
|
||||
* Various updates to documentation.
|
||||
* Don't crash if we can't look up peername
|
||||
* Fix missing string formatting argument
|
||||
* Moved sshuttle/tests into tests.
|
||||
* Updated bandit config.
|
||||
* Replace path /dev/null by os.devnull.
|
||||
* Added coverage report to tests.
|
||||
* Fixes support for OpenBSD (6.1+) (#282).
|
||||
* Close stdin, stdout, and stderr when using syslog or forking to daemon (#283).
|
||||
* Changes pf exclusion rules precedence.
|
||||
* Fix deadlock with iptables with large ruleset.
|
||||
* docs: document --ns-hosts --to-ns and update --dns.
|
||||
* Use subprocess.check_output instead of run.
|
||||
* Fix potential deadlock condition in nft_get_handle.
|
||||
* auto-nets: retrieve routes only if using auto-nets.
|
||||
|
||||
|
||||
0.78.4 - 2018-04-02
|
||||
-------------------
|
||||
|
||||
|
36
README.rst
36
README.rst
@ -23,8 +23,9 @@ common case:
|
||||
|
||||
- You can't use openssh's PermitTunnel feature because
|
||||
it's disabled by default on openssh servers; plus it does
|
||||
TCP-over-TCP, which has terrible performance (see below).
|
||||
TCP-over-TCP, which has `terrible performance`_.
|
||||
|
||||
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
|
||||
|
||||
Obtaining sshuttle
|
||||
------------------
|
||||
@ -33,6 +34,18 @@ Obtaining sshuttle
|
||||
|
||||
apt-get install sshuttle
|
||||
|
||||
- Arch Linux::
|
||||
|
||||
pacman -S sshuttle
|
||||
|
||||
- Fedora::
|
||||
|
||||
dnf install sshuttle
|
||||
|
||||
- NixOS::
|
||||
|
||||
nix-env -iA nixos.sshuttle
|
||||
|
||||
- From PyPI::
|
||||
|
||||
sudo pip install sshuttle
|
||||
@ -43,6 +56,13 @@ Obtaining sshuttle
|
||||
cd sshuttle
|
||||
sudo ./setup.py install
|
||||
|
||||
- FreeBSD::
|
||||
|
||||
# ports
|
||||
cd /usr/ports/net/py-sshuttle && make install clean
|
||||
# pkg
|
||||
pkg install py36-sshuttle
|
||||
|
||||
It is also possible to install into a virtualenv as a non-root user.
|
||||
|
||||
- From PyPI::
|
||||
@ -63,11 +83,21 @@ It is also possible to install into a virtualenv as a non-root user.
|
||||
|
||||
brew install sshuttle
|
||||
|
||||
- Nix::
|
||||
|
||||
nix-env -iA nixpkgs.sshuttle
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
The documentation for the stable version is available at:
|
||||
http://sshuttle.readthedocs.org/
|
||||
https://sshuttle.readthedocs.org/
|
||||
|
||||
The documentation for the latest development version is available at:
|
||||
http://sshuttle.readthedocs.org/en/latest/
|
||||
https://sshuttle.readthedocs.org/en/latest/
|
||||
|
||||
|
||||
Running as a service
|
||||
--------------------
|
||||
Sshuttle can also be run as a service and configured using a config management system:
|
||||
https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe
|
||||
|
@ -1,5 +1,5 @@
|
||||
exclude_dirs:
|
||||
- sshuttle/tests
|
||||
- tests
|
||||
skips:
|
||||
- B101
|
||||
- B104
|
||||
|
76
bin/sudoers-add
Executable file
76
bin/sudoers-add
Executable file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# William Mantly <wmantly@gmail.com>
|
||||
# MIT License
|
||||
# https://github.com/wmantly/sudoers-add
|
||||
|
||||
NEWLINE=$'\n'
|
||||
CONTENT=""
|
||||
ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")"
|
||||
|
||||
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
|
||||
echo "Usage: $ME [file_path] [sudoers-file-name]"
|
||||
echo "Usage: [content] | $ME sudoers-file-name"
|
||||
echo "This will take a sudoers config validate it and add it to /etc/sudoers.d/{sudoers-file-name}"
|
||||
echo "The config can come from a file, first usage example or piped in second example."
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" == "" ]; then
|
||||
(>&2 echo "This command take at lest one argument. See $ME --help")
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$2" == "" ]; then
|
||||
FILE_NAME=$1
|
||||
shift
|
||||
else
|
||||
FILE_NAME=$2
|
||||
fi
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while read -r line
|
||||
do
|
||||
CONTENT+="${line}${NEWLINE}"
|
||||
done < "${1:-/dev/stdin}"
|
||||
|
||||
if [ "$CONTENT" == "" ]; then
|
||||
(>&2 echo "No config content specified. See $ME --help")
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$FILE_NAME" == "" ]; then
|
||||
(>&2 echo "No sudoers file name specified. See $ME --help")
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make a temp file to hold the sudoers config
|
||||
umask 077
|
||||
TEMP_FILE=$(mktemp)
|
||||
echo "$CONTENT" > "$TEMP_FILE"
|
||||
|
||||
# Make sure the content is valid
|
||||
visudo_STDOUT=$(visudo -c -f "$TEMP_FILE" 2>&1)
|
||||
visudo_code=$?
|
||||
# The temp file is no longer needed
|
||||
rm "$TEMP_FILE"
|
||||
|
||||
if [ $visudo_code -eq 0 ]; then
|
||||
echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME"
|
||||
chmod 0440 "/etc/sudoers.d/$FILE_NAME"
|
||||
echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!"
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "Invalid sudoers config!"
|
||||
echo "$visudo_STDOUT"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
10
conftest.py
10
conftest.py
@ -1,10 +0,0 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
good_python = sys.version_info >= (3, 5)
|
||||
else:
|
||||
good_python = sys.version_info >= (2, 7)
|
||||
|
||||
collect_ignore = []
|
||||
if not good_python:
|
||||
collect_ignore.append("sshuttle/tests/client")
|
12
docs/chromeos.rst
Normal file
12
docs/chromeos.rst
Normal file
@ -0,0 +1,12 @@
|
||||
Google ChromeOS
|
||||
===============
|
||||
|
||||
Currently there is no built in support for running sshuttle directly on
|
||||
Google ChromeOS/Chromebooks.
|
||||
|
||||
What we can really do is to create a Linux VM with Crostini. In the default
|
||||
stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and
|
||||
it just works, as do xterms and ssvncviewer etc.
|
||||
|
||||
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide
|
||||
|
@ -5,8 +5,18 @@ Installation
|
||||
|
||||
pip install sshuttle
|
||||
|
||||
- Debain package manager::
|
||||
sudo apt install sshuttle
|
||||
|
||||
- Clone::
|
||||
|
||||
git clone https://github.com/sshuttle/sshuttle.git
|
||||
cd sshuttle
|
||||
./setup.py install
|
||||
|
||||
|
||||
Optionally after installation
|
||||
-----------------------------
|
||||
|
||||
- Add to sudoers file
|
||||
sshuttle --sudoers
|
||||
|
@ -28,7 +28,7 @@ Options
|
||||
-------
|
||||
.. program:: sshuttle
|
||||
|
||||
.. option:: subnets
|
||||
.. option:: <subnets>
|
||||
|
||||
A list of subnets to route over the VPN, in the form
|
||||
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
|
||||
@ -44,13 +44,13 @@ Options
|
||||
to during startup will be routed over the VPN. Valid examples are
|
||||
example.com, example.com:8000 and example.com:8000-9000.
|
||||
|
||||
.. option:: --method [auto|nat|tproxy|pf]
|
||||
.. option:: --method <auto|nat|nft|tproxy|pf>
|
||||
|
||||
Which firewall method should sshuttle use? For auto, sshuttle attempts to
|
||||
guess the appropriate method depending on what it can find in PATH. The
|
||||
default value is auto.
|
||||
|
||||
.. option:: -l, --listen=[ip:]port
|
||||
.. option:: -l <[ip:]port>, --listen=<[ip:]port>
|
||||
|
||||
Use this ip address and port number as the transparent
|
||||
proxy port. By default :program:`sshuttle` finds an available
|
||||
@ -94,7 +94,30 @@ Options
|
||||
.. option:: --dns
|
||||
|
||||
Capture local DNS requests and forward to the remote DNS
|
||||
server.
|
||||
server. All queries to any of the local system's DNS
|
||||
servers (/etc/resolv.conf) will be intercepted and
|
||||
resolved on the remote side of the tunnel instead, there
|
||||
using the DNS specified via the :option:`--to-ns` option,
|
||||
if specified.
|
||||
|
||||
.. option:: --ns-hosts=<server1[,server2[,server3[...]]]>
|
||||
|
||||
Capture local DNS requests to the specified server(s)
|
||||
and forward to the remote DNS server. Contrary to the
|
||||
:option:`--dns` option, this flag allows to specify the
|
||||
DNS server(s) the queries to which to intercept,
|
||||
instead of intercepting all DNS traffic on the local
|
||||
machine. This can be useful when only certain DNS
|
||||
requests should be resolved on the remote side of the
|
||||
tunnel, e.g. in combination with dnsmasq.
|
||||
|
||||
.. option:: --to-ns=<server>
|
||||
|
||||
The DNS to forward requests to when remote DNS
|
||||
resolution is enabled. If not given, sshuttle will
|
||||
simply resolve using the system configured resolver on
|
||||
the remote side (via /etc/resolv.conf on the remote
|
||||
side).
|
||||
|
||||
.. option:: --python
|
||||
|
||||
@ -102,14 +125,14 @@ Options
|
||||
The default is just ``python``, which means to use the
|
||||
default python interpreter on the remote system's PATH.
|
||||
|
||||
.. option:: -r, --remote=[username@]sshserver[:port]
|
||||
.. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
|
||||
|
||||
The remote hostname and optional username and ssh
|
||||
port number to use for connecting to the remote server.
|
||||
For example, example.com, testuser@example.com,
|
||||
testuser@example.com:2222, or example.com:2244.
|
||||
|
||||
.. option:: -x, --exclude=subnet
|
||||
.. option:: -x <subnet>, --exclude=<subnet>
|
||||
|
||||
Explicitly exclude this subnet from forwarding. The
|
||||
format of this option is the same as the ``<subnets>``
|
||||
@ -118,7 +141,7 @@ Options
|
||||
``0/0 -x 1.2.3.0/24`` to forward everything except the
|
||||
local subnet over the VPN, for example.
|
||||
|
||||
.. option:: -X, --exclude-from=file
|
||||
.. option:: -X <file>, --exclude-from=<file>
|
||||
|
||||
Exclude the subnets specified in a file, one subnet per
|
||||
line. Useful when you have lots of subnets to exclude.
|
||||
@ -166,6 +189,13 @@ Options
|
||||
control feature, maximizing bandwidth usage. Use at
|
||||
your own risk.
|
||||
|
||||
.. option:: --latency-buffer-size
|
||||
|
||||
Set the size of the buffer used in latency control. The
|
||||
default is ``32768``. Changing this option allows a compromise
|
||||
to be made between latency and bandwidth without completely
|
||||
disabling latency control (with :option:`--no-latency-control`).
|
||||
|
||||
.. option:: -D, --daemon
|
||||
|
||||
Automatically fork into the background after connecting
|
||||
@ -177,7 +207,7 @@ Options
|
||||
:manpage:`syslog(3)` service instead of stderr. This is
|
||||
implicit if you use :option:`--daemon`.
|
||||
|
||||
.. option:: --pidfile=pidfilename
|
||||
.. option:: --pidfile=<pidfilename>
|
||||
|
||||
when using :option:`--daemon`, save :program:`sshuttle`'s pid to
|
||||
*pidfilename*. The default is ``sshuttle.pid`` in the
|
||||
@ -204,17 +234,44 @@ Options
|
||||
makes it a lot easier to debug and test the :option:`--auto-hosts`
|
||||
feature.
|
||||
|
||||
.. option:: --sudoers
|
||||
|
||||
sshuttle will auto generate the proper sudoers.d config file and add it.
|
||||
Once this is completed, sshuttle will exit and tell the user if
|
||||
it succeed or not. Do not call this options with sudo, it may generate a
|
||||
incorrect config file.
|
||||
|
||||
.. option:: --sudoers-no-modify
|
||||
|
||||
sshuttle will auto generate the proper sudoers.d config and print it to
|
||||
stdout. The option will not modify the system at all.
|
||||
|
||||
.. option:: --sudoers-user
|
||||
|
||||
Set the user name or group with %group_name for passwordless operation.
|
||||
Default is the current user.set ALL for all users. Only works with
|
||||
--sudoers or --sudoers-no-modify option.
|
||||
|
||||
.. option:: --sudoers-filename
|
||||
|
||||
Set the file name for the sudoers.d file to be added. Default is
|
||||
"sshuttle_auto". Only works with --sudoers.
|
||||
|
||||
.. option:: --version
|
||||
|
||||
Print program version.
|
||||
|
||||
|
||||
Configuration File
|
||||
------------------
|
||||
All the options described above can optionally be specified in a configuration
|
||||
file.
|
||||
|
||||
To run :program:`sshuttle` with options defined in, e.g., `/etc/ssshuttle.conf`
|
||||
To run :program:`sshuttle` with options defined in, e.g., `/etc/sshuttle.conf`
|
||||
just pass the path to the file preceded by the `@` character, e.g.
|
||||
:option:`@/etc/ssshuttle.conf`.
|
||||
`@/etc/sshuttle.conf`.
|
||||
|
||||
When running :program:`sshuttle` with options defined in a configuratio file,
|
||||
When running :program:`sshuttle` with options defined in a configuration file,
|
||||
options can still be passed via the command line in addition to what is
|
||||
defined in the file. If a given option is defined both in the file and in
|
||||
the command line, the value in the command line will take precedence.
|
||||
|
8
docs/openwrt.rst
Normal file
8
docs/openwrt.rst
Normal file
@ -0,0 +1,8 @@
|
||||
OpenWRT
|
||||
========
|
||||
|
||||
Run::
|
||||
|
||||
opkg install python3 python3-pip iptables-mod-nat-extra iptables-mod-ipopt
|
||||
python3 /usr/bin/pip3 install sshuttle
|
||||
sshuttle -l 0.0.0.0 -r <IP> -x 192.168.1.1 0/0
|
@ -6,5 +6,7 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
chromeos
|
||||
tproxy
|
||||
windows
|
||||
openwrt
|
||||
|
@ -6,7 +6,7 @@ Client side Requirements
|
||||
|
||||
- sudo, or root access on your client machine.
|
||||
(The server doesn't need admin access.)
|
||||
- Python 2.7 or Python 3.5.
|
||||
- Python 3.5 or greater.
|
||||
|
||||
|
||||
Linux with NAT method
|
||||
@ -32,14 +32,6 @@ Supports:
|
||||
* IPv6 UDP (requires ``recvmsg`` - see below)
|
||||
* IPv6 DNS (requires ``recvmsg`` - see below)
|
||||
|
||||
.. _PyXAPI: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
|
||||
|
||||
Full UDP or DNS support with the TPROXY method requires the ``recvmsg()``
|
||||
syscall. This is not available in Python 2, however it is in Python 3.5 and
|
||||
later. Under Python 2 you might find it sufficient to install PyXAPI_ in
|
||||
order to get the ``recvmsg()`` function. See :doc:`tproxy` for more
|
||||
information.
|
||||
|
||||
|
||||
MacOS / FreeBSD / OpenBSD / pfSense
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -73,8 +65,6 @@ possible as support for older versions might be dropped in the future.
|
||||
Additional Suggested Software
|
||||
-----------------------------
|
||||
|
||||
- You may want to use autossh, available in various package management
|
||||
systems.
|
||||
- If you are using systemd, sshuttle can notify it when the connection to
|
||||
the remote end is established and the firewall rules are installed. For
|
||||
this feature to work you must configure the process start-up type for the
|
||||
|
@ -20,6 +20,11 @@ Forward all traffic::
|
||||
|
||||
sshuttle -r username@sshserver 0/0
|
||||
|
||||
|
||||
- For 'My VPN broke and need a temporary solution FAST to access local IPv4 addresses':
|
||||
|
||||
sshuttle --dns -NHr username@sshserver 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||
|
||||
If you would also like your DNS queries to be proxied
|
||||
through the DNS server of the server you are connect to::
|
||||
|
||||
@ -60,3 +65,46 @@ the data back and forth through ssh.
|
||||
Fun, right? A poor man's instant VPN, and you don't even have to have
|
||||
admin access on the server.
|
||||
|
||||
Sudoers File
|
||||
------------
|
||||
sshuttle can auto-generate the proper sudoers.d file using the current user
|
||||
for Linux and OSX. Doing this will allow sshuttle to run without asking for
|
||||
the local sudo password and to give users who do not have sudo access
|
||||
ability to run sshuttle.
|
||||
|
||||
sshuttle --sudoers
|
||||
|
||||
DO NOT run this command with sudo, it will ask for your sudo password when
|
||||
it is needed.
|
||||
|
||||
A costume user or group can be set with the :
|
||||
option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid
|
||||
values for this vary based on how your system is configured. Values such as
|
||||
usernames, groups pre-pended with `%` and sudoers user aliases will work. See
|
||||
the sudoers manual for more information on valid user specif actions.
|
||||
The options must be used with `--sudoers`
|
||||
|
||||
sshuttle --sudoers --sudoers-user mike
|
||||
sshuttle --sudoers --sudoers-user %sudo
|
||||
|
||||
The name of the file to be added to sudoers.d can be configured as well. This
|
||||
is mostly not necessary but can be useful for giving more than one user
|
||||
access to sshuttle. The default is `sshuttle_auto`
|
||||
|
||||
sshuttle --sudoer --sudoers-filename sshuttle_auto_mike
|
||||
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
|
||||
|
||||
You can also see what configuration will be added to your system without
|
||||
modifying anything. This can be helpfull is the auto feature does not work, or
|
||||
you want more control. This option also works with `--sudoers-username`.
|
||||
`--sudoers-filename` has no effect with this option.
|
||||
|
||||
sshuttle --sudoers-no-modify
|
||||
|
||||
This will simply sprint the generated configuration to STDOUT. Example
|
||||
|
||||
08:40 PM william$ sshuttle --sudoers-no-modify
|
||||
|
||||
Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall
|
||||
|
||||
william ALL=NOPASSWD: SSHUTTLE304
|
||||
|
@ -1,4 +1,7 @@
|
||||
-r requirements.txt
|
||||
pytest==3.4.2
|
||||
attrs==19.3.0
|
||||
pytest==5.4.3
|
||||
pytest-cov==2.9.0
|
||||
mock==2.0.0
|
||||
flake8==3.5.0
|
||||
flake8==3.8.2
|
||||
pyflakes==2.2.0
|
||||
|
@ -1 +1 @@
|
||||
setuptools-scm==1.15.6
|
||||
setuptools-scm==4.1.2
|
||||
|
3
run
3
run
@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
export PYTHONPATH="$(dirname $0):$PYTHONPATH"
|
||||
export PATH="$(dirname $0)/bin:$PATH"
|
||||
|
||||
python_best_version() {
|
||||
if [ -x "$(command -v python3)" ] &&
|
||||
python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
|
||||
exec python3 "$@"
|
||||
elif [ -x "$(command -v python2.7)" ]; then
|
||||
exec python2.7 "$@"
|
||||
else
|
||||
exec python "$@"
|
||||
fi
|
||||
|
@ -7,3 +7,11 @@ universal = 1
|
||||
[upload]
|
||||
sign=true
|
||||
identity=0x1784577F811F6EAC
|
||||
|
||||
[flake8]
|
||||
count=true
|
||||
show-source=true
|
||||
statistics=true
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=sshuttle --cov-branch --cov-report=term-missing
|
||||
|
14
setup.py
14
setup.py
@ -41,6 +41,7 @@ setup(
|
||||
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",
|
||||
@ -48,15 +49,24 @@ setup(
|
||||
"License :: OSI Approved :: "
|
||||
"GNU Lesser General Public License v2 or later (LGPLv2+)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Topic :: System :: Networking",
|
||||
],
|
||||
scripts=['bin/sudoers-add'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'sshuttle = sshuttle.cmdline:main',
|
||||
],
|
||||
},
|
||||
tests_require=['pytest', 'pytest-runner', 'mock'],
|
||||
tests_require=[
|
||||
'pytest',
|
||||
'pytest-cov',
|
||||
'pytest-runner',
|
||||
'mock',
|
||||
'flake8',
|
||||
],
|
||||
keywords="ssh vpn",
|
||||
)
|
||||
|
@ -30,9 +30,12 @@ while 1:
|
||||
sys.stderr.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
import sshuttle.helpers
|
||||
# import can only happen once the code has been transferred to
|
||||
# the server. 'noqa: E402' excludes these lines from QA checks.
|
||||
import sshuttle.helpers # noqa: E402
|
||||
sshuttle.helpers.verbose = verbosity
|
||||
|
||||
import sshuttle.cmdline_options as options
|
||||
from sshuttle.server import main
|
||||
main(options.latency_control, options.auto_hosts, options.to_nameserver)
|
||||
import sshuttle.cmdline_options as options # noqa: E402
|
||||
from sshuttle.server import main # noqa: E402
|
||||
main(options.latency_control, options.auto_hosts, options.to_nameserver,
|
||||
options.auto_nets)
|
||||
|
@ -3,13 +3,15 @@ import re
|
||||
import signal
|
||||
import time
|
||||
import subprocess as ssubprocess
|
||||
import sshuttle.helpers as helpers
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
|
||||
import sshuttle.helpers as helpers
|
||||
import sshuttle.ssnet as ssnet
|
||||
import sshuttle.ssh as ssh
|
||||
import sshuttle.ssyslog as ssyslog
|
||||
import sys
|
||||
import platform
|
||||
import sshuttle.sdnotify as sdnotify
|
||||
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
||||
resolvconf_nameservers
|
||||
@ -33,7 +35,7 @@ except AttributeError:
|
||||
except ImportError:
|
||||
import socket
|
||||
|
||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
||||
|
||||
|
||||
def got_signal(signum, frame):
|
||||
@ -93,7 +95,7 @@ def daemonize():
|
||||
# be deleted.
|
||||
signal.signal(signal.SIGTERM, got_signal)
|
||||
|
||||
si = open('/dev/null', 'r+')
|
||||
si = open(os.devnull, 'r+')
|
||||
os.dup2(si.fileno(), 0)
|
||||
os.dup2(si.fileno(), 1)
|
||||
si.close()
|
||||
@ -183,7 +185,7 @@ class MultiListener:
|
||||
|
||||
class FirewallClient:
|
||||
|
||||
def __init__(self, method_name):
|
||||
def __init__(self, method_name, sudo_pythonpath):
|
||||
self.auto_nets = []
|
||||
python_path = os.path.dirname(os.path.dirname(__file__))
|
||||
argvbase = ([sys.executable, sys.argv[0]] +
|
||||
@ -192,11 +194,15 @@ class FirewallClient:
|
||||
['--firewall'])
|
||||
if ssyslog._p:
|
||||
argvbase += ['--syslog']
|
||||
argv_tries = [
|
||||
['sudo', '-p', '[local sudo] Password: ', '/usr/bin/env',
|
||||
('PYTHONPATH=%s' % python_path)] + argvbase,
|
||||
argvbase
|
||||
]
|
||||
# Default to sudo unless on OpenBSD in which case use built in `doas`
|
||||
if platform.platform().startswith('OpenBSD'):
|
||||
elev_prefix = ['doas']
|
||||
else:
|
||||
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
|
||||
if sudo_pythonpath:
|
||||
elev_prefix += ['/usr/bin/env',
|
||||
'PYTHONPATH=%s' % python_path]
|
||||
argv_tries = [elev_prefix + argvbase, argvbase]
|
||||
|
||||
# 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.
|
||||
@ -215,18 +221,14 @@ class FirewallClient:
|
||||
if argv[0] == 'su':
|
||||
sys.stderr.write('[local su] ')
|
||||
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
||||
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
||||
e = None
|
||||
break
|
||||
except OSError as e:
|
||||
except OSError:
|
||||
pass
|
||||
self.argv = argv
|
||||
s1.close()
|
||||
if sys.version_info < (3, 0):
|
||||
# python 2.7
|
||||
self.pfile = s2.makefile('wb+')
|
||||
else:
|
||||
# python 3.5
|
||||
self.pfile = s2.makefile('rwb')
|
||||
self.pfile = s2.makefile('rwb')
|
||||
if e:
|
||||
log('Spawning firewall manager: %r\n' % self.argv)
|
||||
raise Fatal(e)
|
||||
@ -260,11 +262,13 @@ class FirewallClient:
|
||||
self.pfile.write(b'ROUTES\n')
|
||||
for (family, ip, width, fport, lport) \
|
||||
in self.subnets_include + self.auto_nets:
|
||||
self.pfile.write(b'%d,%d,0,%s,%d,%d\n'
|
||||
% (family, width, ip.encode("ASCII"), fport, lport))
|
||||
self.pfile.write(b'%d,%d,0,%s,%d,%d\n' % (family, width,
|
||||
ip.encode("ASCII"),
|
||||
fport, lport))
|
||||
for (family, ip, width, fport, lport) in self.subnets_exclude:
|
||||
self.pfile.write(b'%d,%d,1,%s,%d,%d\n'
|
||||
% (family, width, ip.encode("ASCII"), fport, lport))
|
||||
self.pfile.write(b'%d,%d,1,%s,%d,%d\n' % (family, width,
|
||||
ip.encode("ASCII"),
|
||||
fport, lport))
|
||||
|
||||
self.pfile.write(b'NSLIST\n')
|
||||
for (family, ip) in self.nslist:
|
||||
@ -295,8 +299,8 @@ class FirewallClient:
|
||||
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
|
||||
|
||||
def sethostip(self, hostname, ip):
|
||||
assert(not re.search(b'[^-\w\.]', hostname))
|
||||
assert(not re.search(b'[^0-9.]', ip))
|
||||
assert(not re.search(rb'[^-\w\.]', hostname))
|
||||
assert(not re.search(rb'[^0-9.]', ip))
|
||||
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
|
||||
self.pfile.flush()
|
||||
|
||||
@ -347,7 +351,7 @@ def onaccept_tcp(listener, method, mux, handlers):
|
||||
sock, srcip = listener.accept()
|
||||
sock.close()
|
||||
finally:
|
||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
||||
return
|
||||
else:
|
||||
raise
|
||||
@ -445,13 +449,14 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
stderr=ssyslog._p and ssyslog._p.stdin,
|
||||
options=dict(latency_control=latency_control,
|
||||
auto_hosts=auto_hosts,
|
||||
to_nameserver=to_nameserver))
|
||||
to_nameserver=to_nameserver,
|
||||
auto_nets=auto_nets))
|
||||
except socket.error as e:
|
||||
if e.args[0] == errno.EPIPE:
|
||||
raise Fatal("failed to establish ssh session (1)")
|
||||
else:
|
||||
raise
|
||||
mux = Mux(serversock, serversock)
|
||||
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
|
||||
handlers.append(mux)
|
||||
|
||||
expected = b'SSHUTTLE0001'
|
||||
@ -486,7 +491,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
def onroutes(routestr):
|
||||
if auto_nets:
|
||||
for line in routestr.strip().split(b'\n'):
|
||||
if not line: continue
|
||||
if not line:
|
||||
continue
|
||||
(family, ip, width) = line.split(b',', 2)
|
||||
family = int(family)
|
||||
width = int(width)
|
||||
@ -507,9 +513,14 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
# set --auto-nets, we might as well wait for the message first, then
|
||||
# ignore its contents.
|
||||
mux.got_routes = None
|
||||
fw.start()
|
||||
serverready()
|
||||
|
||||
mux.got_routes = onroutes
|
||||
|
||||
def serverready():
|
||||
fw.start()
|
||||
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
|
||||
|
||||
def onhostlist(hostlist):
|
||||
debug2('got host list: %r\n' % hostlist)
|
||||
for line in hostlist.strip().split():
|
||||
@ -544,7 +555,7 @@ def main(listenip_v6, listenip_v4,
|
||||
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
||||
method_name, seed_hosts, auto_hosts, auto_nets,
|
||||
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
||||
user):
|
||||
user, sudo_pythonpath):
|
||||
|
||||
if daemon:
|
||||
try:
|
||||
@ -554,7 +565,7 @@ def main(listenip_v6, listenip_v4,
|
||||
return 5
|
||||
debug1('Starting sshuttle proxy.\n')
|
||||
|
||||
fw = FirewallClient(method_name)
|
||||
fw = FirewallClient(method_name, sudo_pythonpath)
|
||||
|
||||
# Get family specific subnet lists
|
||||
if dns:
|
||||
@ -589,8 +600,13 @@ def main(listenip_v6, listenip_v4,
|
||||
except KeyError:
|
||||
raise Fatal("User %s does not exist." % user)
|
||||
|
||||
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
|
||||
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
|
||||
if fw.method.name != 'nat':
|
||||
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
|
||||
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
|
||||
else:
|
||||
required.ipv6 = None
|
||||
required.ipv4 = None
|
||||
|
||||
required.udp = avail.udp
|
||||
required.dns = len(nslist) > 0
|
||||
required.user = False if user is None else True
|
||||
@ -698,7 +714,8 @@ def main(listenip_v6, listenip_v4,
|
||||
ports = range(12300, 9000, -1)
|
||||
for port in ports:
|
||||
debug2(' %d' % port)
|
||||
if port in used_ports: continue
|
||||
if port in used_ports:
|
||||
continue
|
||||
|
||||
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
||||
|
||||
@ -782,6 +799,8 @@ def main(listenip_v6, listenip_v4,
|
||||
# it's not our child anymore; can't waitpid
|
||||
fw.p.returncode = 0
|
||||
fw.done()
|
||||
sdnotify.send(sdnotify.stop())
|
||||
|
||||
finally:
|
||||
if daemon:
|
||||
daemon_cleanup()
|
||||
|
@ -1,5 +1,6 @@
|
||||
import re
|
||||
import socket
|
||||
import platform
|
||||
import sshuttle.helpers as helpers
|
||||
import sshuttle.client as client
|
||||
import sshuttle.firewall as firewall
|
||||
@ -7,16 +8,35 @@ import sshuttle.hostwatch as hostwatch
|
||||
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
|
||||
|
||||
|
||||
def main():
|
||||
opt = parser.parse_args()
|
||||
|
||||
if opt.sudoers or opt.sudoers_no_modify:
|
||||
if platform.platform().startswith('OpenBSD'):
|
||||
log('Automatic sudoers does not work on BSD')
|
||||
exit(1)
|
||||
|
||||
if not opt.sudoers_filename:
|
||||
log('--sudoers-file must be set or omited.')
|
||||
exit(1)
|
||||
|
||||
sudoers(
|
||||
user_name=opt.sudoers_user,
|
||||
no_modify=opt.sudoers_no_modify,
|
||||
file_name=opt.sudoers_filename
|
||||
)
|
||||
|
||||
if opt.daemon:
|
||||
opt.syslog = 1
|
||||
if opt.wrap:
|
||||
import sshuttle.ssnet as ssnet
|
||||
ssnet.MAX_CHANNEL = opt.wrap
|
||||
if opt.latency_buffer_size:
|
||||
import sshuttle.ssnet as ssnet
|
||||
ssnet.LATENCY_BUFFER_SIZE = opt.latency_buffer_size
|
||||
helpers.verbose = opt.verbose
|
||||
|
||||
try:
|
||||
@ -59,6 +79,8 @@ def main():
|
||||
ipport_v6 = "auto" if not opt.disable_ipv6 else None
|
||||
if opt.syslog:
|
||||
ssyslog.start_syslog()
|
||||
ssyslog.close_stdin()
|
||||
ssyslog.stdout_to_syslog()
|
||||
ssyslog.stderr_to_syslog()
|
||||
return_code = client.main(ipport_v6, ipport_v4,
|
||||
opt.ssh_cmd,
|
||||
@ -76,12 +98,13 @@ def main():
|
||||
opt.daemon,
|
||||
opt.to_ns,
|
||||
opt.pidfile,
|
||||
opt.user)
|
||||
opt.user,
|
||||
opt.sudo_pythonpath)
|
||||
|
||||
if return_code == 0:
|
||||
log('Normal exit code, exiting...')
|
||||
else:
|
||||
log('Abnormal exit code detected, failing...' % return_code)
|
||||
log('Abnormal exit code %d detected, failing...' % return_code)
|
||||
return return_code
|
||||
|
||||
except Fatal as e:
|
||||
|
@ -1,12 +1,12 @@
|
||||
import errno
|
||||
import socket
|
||||
import signal
|
||||
import sshuttle.ssyslog as ssyslog
|
||||
import sshuttle.sdnotify as sdnotify
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import traceback
|
||||
|
||||
import sshuttle.ssyslog as ssyslog
|
||||
from sshuttle.helpers import debug1, debug2, Fatal
|
||||
from sshuttle.methods import get_auto_method, get_method
|
||||
|
||||
@ -132,7 +132,7 @@ def main(method_name, syslog):
|
||||
try:
|
||||
(family, width, exclude, ip, fport, lport) = \
|
||||
line.strip().split(',', 5)
|
||||
except:
|
||||
except BaseException:
|
||||
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
|
||||
subnets.append((
|
||||
int(family),
|
||||
@ -154,7 +154,7 @@ def main(method_name, syslog):
|
||||
break
|
||||
try:
|
||||
(family, ip) = line.strip().split(',', 1)
|
||||
except:
|
||||
except BaseException:
|
||||
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
|
||||
nslist.append((int(family), ip))
|
||||
debug2('firewall manager: Got partial nslist: %r\n' % nslist)
|
||||
@ -219,8 +219,6 @@ def main(method_name, syslog):
|
||||
user)
|
||||
|
||||
stdout.write('STARTED\n')
|
||||
sdnotify.send(sdnotify.ready(),
|
||||
sdnotify.status('Connected'))
|
||||
|
||||
try:
|
||||
stdout.flush()
|
||||
@ -246,45 +244,44 @@ def main(method_name, syslog):
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
sdnotify.send(sdnotify.stop())
|
||||
debug1('firewall manager: undoing changes.\n')
|
||||
except:
|
||||
pass
|
||||
except BaseException:
|
||||
debug2('An error occurred, ignoring it.')
|
||||
|
||||
try:
|
||||
if subnets_v6 or nslist_v6:
|
||||
debug2('firewall manager: undoing IPv6 changes.\n')
|
||||
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
|
||||
except:
|
||||
except BaseException:
|
||||
try:
|
||||
debug1("firewall manager: "
|
||||
"Error trying to undo IPv6 firewall.\n")
|
||||
for line in traceback.format_exc().splitlines():
|
||||
debug1("---> %s\n" % line)
|
||||
except:
|
||||
pass
|
||||
except BaseException:
|
||||
debug2('An error occurred, ignoring it.')
|
||||
|
||||
try:
|
||||
if subnets_v4 or nslist_v4:
|
||||
debug2('firewall manager: undoing IPv4 changes.\n')
|
||||
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
|
||||
except:
|
||||
except BaseException:
|
||||
try:
|
||||
debug1("firewall manager: "
|
||||
"Error trying to undo IPv4 firewall.\n")
|
||||
for line in traceback.format_exc().splitlines():
|
||||
debug1("firewall manager: ---> %s\n" % line)
|
||||
except:
|
||||
pass
|
||||
except BaseException:
|
||||
debug2('An error occurred, ignoring it.')
|
||||
|
||||
try:
|
||||
debug2('firewall manager: undoing /etc/hosts changes.\n')
|
||||
restore_etc_hosts(port_v6 or port_v4)
|
||||
except:
|
||||
except BaseException:
|
||||
try:
|
||||
debug1("firewall manager: "
|
||||
"Error trying to undo /etc/hosts changes.\n")
|
||||
for line in traceback.format_exc().splitlines():
|
||||
debug1("firewall manager: ---> %s\n" % line)
|
||||
except:
|
||||
pass
|
||||
except BaseException:
|
||||
debug2('An error occurred, ignoring it.')
|
||||
|
@ -5,16 +5,9 @@ import errno
|
||||
logprefix = ''
|
||||
verbose = 0
|
||||
|
||||
if sys.version_info[0] == 3:
|
||||
binary_type = bytes
|
||||
|
||||
def b(s):
|
||||
return s.encode("ASCII")
|
||||
else:
|
||||
binary_type = str
|
||||
|
||||
def b(s):
|
||||
return s
|
||||
def b(s):
|
||||
return s.encode("ASCII")
|
||||
|
||||
|
||||
def log(s):
|
||||
@ -56,22 +49,22 @@ class Fatal(Exception):
|
||||
|
||||
|
||||
def resolvconf_nameservers():
|
||||
l = []
|
||||
lines = []
|
||||
for line in open('/etc/resolv.conf'):
|
||||
words = line.lower().split()
|
||||
if len(words) >= 2 and words[0] == 'nameserver':
|
||||
l.append(family_ip_tuple(words[1]))
|
||||
return l
|
||||
lines.append(family_ip_tuple(words[1]))
|
||||
return lines
|
||||
|
||||
|
||||
def resolvconf_random_nameserver():
|
||||
l = resolvconf_nameservers()
|
||||
if l:
|
||||
if len(l) > 1:
|
||||
lines = resolvconf_nameservers()
|
||||
if lines:
|
||||
if len(lines) > 1:
|
||||
# don't import this unless we really need it
|
||||
import random
|
||||
random.shuffle(l)
|
||||
return l[0]
|
||||
random.shuffle(lines)
|
||||
return lines[0]
|
||||
else:
|
||||
return (socket.AF_INET, '127.0.0.1')
|
||||
|
||||
|
@ -21,7 +21,7 @@ _smb_ok = True
|
||||
hostnames = {}
|
||||
queue = {}
|
||||
try:
|
||||
null = open('/dev/null', 'wb')
|
||||
null = open(os.devnull, 'wb')
|
||||
except IOError:
|
||||
_, e = sys.exc_info()[:2]
|
||||
log('warning: %s\n' % e)
|
||||
@ -44,7 +44,7 @@ def write_host_cache():
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmpname)
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ def _check_revdns(ip):
|
||||
debug3('< %s\n' % r[0])
|
||||
check_host(r[0])
|
||||
found_host(r[0], ip)
|
||||
except socket.herror:
|
||||
except (socket.herror, UnicodeError):
|
||||
pass
|
||||
|
||||
|
||||
@ -119,15 +119,20 @@ def _check_dns(hostname):
|
||||
debug3('< %s\n' % ip)
|
||||
check_host(ip)
|
||||
found_host(hostname, ip)
|
||||
except socket.gaierror:
|
||||
except (socket.gaierror, UnicodeError):
|
||||
pass
|
||||
|
||||
|
||||
def _check_netstat():
|
||||
debug2(' > netstat\n')
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
argv = ['netstat', '-n']
|
||||
try:
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||
env=env)
|
||||
content = p.stdout.read().decode("ASCII")
|
||||
p.wait()
|
||||
except OSError:
|
||||
@ -145,10 +150,15 @@ def _check_smb(hostname):
|
||||
global _smb_ok
|
||||
if not _smb_ok:
|
||||
return
|
||||
argv = ['smbclient', '-U', '%', '-L', hostname]
|
||||
debug2(' > smb: %s\n' % hostname)
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
argv = ['smbclient', '-U', '%', '-L', hostname]
|
||||
try:
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||
env=env)
|
||||
lines = p.stdout.readlines()
|
||||
p.wait()
|
||||
except OSError:
|
||||
@ -203,10 +213,15 @@ def _check_nmb(hostname, is_workgroup, is_master):
|
||||
global _nmb_ok
|
||||
if not _nmb_ok:
|
||||
return
|
||||
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
|
||||
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
|
||||
try:
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||
env=env)
|
||||
lines = p.stdout.readlines()
|
||||
rv = p.wait()
|
||||
except OSError:
|
||||
|
@ -1,4 +1,3 @@
|
||||
import re
|
||||
import os
|
||||
import socket
|
||||
import subprocess as ssubprocess
|
||||
@ -24,13 +23,13 @@ def ipt_chain_exists(family, table, name):
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||
for line in p.stdout:
|
||||
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
|
||||
return True
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
try:
|
||||
output = ssubprocess.check_output(argv, env=env)
|
||||
for line in output.decode('ASCII').split('\n'):
|
||||
if line.startswith('Chain %s ' % name):
|
||||
return True
|
||||
except ssubprocess.CalledProcessError as e:
|
||||
raise Fatal('%r returned %d' % (argv, e.returncode))
|
||||
|
||||
|
||||
def ipt(family, table, *args):
|
||||
@ -51,10 +50,8 @@ def ipt(family, table, *args):
|
||||
|
||||
|
||||
def nft(family, table, action, *args):
|
||||
if family == socket.AF_INET:
|
||||
argv = ['nft', action, 'ip', table] + list(args)
|
||||
elif family == socket.AF_INET6:
|
||||
argv = ['nft', action, 'ip6', table] + list(args)
|
||||
if family in (socket.AF_INET, socket.AF_INET6):
|
||||
argv = ['nft', action, 'inet', table] + list(args)
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
@ -67,22 +64,6 @@ def nft(family, table, action, *args):
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def nft_get_handle(expression, chain):
|
||||
cmd = 'nft'
|
||||
argv = [cmd, 'list', expression, '-a']
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||
for line in p.stdout:
|
||||
if (b'jump %s' % chain.encode('utf-8')) in line:
|
||||
return re.sub('.*# ', '', line.decode('utf-8'))
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
_no_ttl_module = False
|
||||
|
||||
|
||||
|
@ -29,8 +29,8 @@ IPV6_RECVDSTADDR = 74
|
||||
if recvmsg == "python":
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP python using recvmsg.\n')
|
||||
data, ancdata, _, srcip = \
|
||||
listener.recvmsg(4096, socket.CMSG_SPACE(4))
|
||||
data, ancdata, _, srcip = listener.recvmsg(4096,
|
||||
socket.CMSG_SPACE(4))
|
||||
dstip = None
|
||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
||||
@ -42,8 +42,8 @@ if recvmsg == "python":
|
||||
elif recvmsg == "socket_ext":
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP using socket_ext recvmsg.\n')
|
||||
srcip, data, adata, _ = \
|
||||
listener.recvmsg((bufsize,), socket.CMSG_SPACE(4))
|
||||
srcip, data, adata, _ = listener.recvmsg((bufsize,),
|
||||
socket.CMSG_SPACE(4))
|
||||
dstip = None
|
||||
for a in adata:
|
||||
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR:
|
||||
@ -105,7 +105,8 @@ def _fill_oldctls(prefix):
|
||||
def _sysctl_set(name, val):
|
||||
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
return ssubprocess.call(argv, stdout=open('/dev/null', 'w'))
|
||||
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'))
|
||||
# No env: No output. (Or error that won't be parsed.)
|
||||
|
||||
|
||||
_changedctls = []
|
||||
@ -134,10 +135,12 @@ def sysctl_set(name, val, permanent=False):
|
||||
_changedctls.append(name)
|
||||
return True
|
||||
|
||||
|
||||
def ipfw(*args):
|
||||
argv = ['ipfw', '-q'] + list(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv)
|
||||
# No env: No output. (Or error that won't be parsed.)
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
@ -146,13 +149,15 @@ def ipfw_noexit(*args):
|
||||
argv = ['ipfw', '-q'] + list(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
ssubprocess.call(argv)
|
||||
# No env: No output. (Or error that won't be parsed.)
|
||||
|
||||
|
||||
class Method(BaseMethod):
|
||||
|
||||
def get_supported_features(self):
|
||||
result = super(Method, self).get_supported_features()
|
||||
result.ipv6 = False
|
||||
result.udp = False #NOTE: Almost there, kernel patch needed
|
||||
result.udp = False # NOTE: Almost there, kernel patch needed
|
||||
result.dns = True
|
||||
return result
|
||||
|
||||
@ -175,21 +180,21 @@ class Method(BaseMethod):
|
||||
"couldn't determine source IP address\n" % (dstip,))
|
||||
return
|
||||
|
||||
#debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
|
||||
# debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
|
||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
sender.bind(srcip)
|
||||
sender.sendto(data,dstip)
|
||||
sender.sendto(data, dstip)
|
||||
sender.close()
|
||||
|
||||
def setup_udp_listener(self, udp_listener):
|
||||
if udp_listener.v4 is not None:
|
||||
udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1)
|
||||
#if udp_listener.v6 is not None:
|
||||
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
||||
# if udp_listener.v6 is not None:
|
||||
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
||||
|
||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||
user):
|
||||
@ -199,7 +204,7 @@ class Method(BaseMethod):
|
||||
'Address family "%s" unsupported by ipfw method_name'
|
||||
% family_to_string(family))
|
||||
|
||||
#XXX: Any risk from this?
|
||||
# XXX: Any risk from this?
|
||||
ipfw_noexit('delete', '1')
|
||||
|
||||
while _changedctls:
|
||||
@ -238,8 +243,9 @@ class Method(BaseMethod):
|
||||
|
||||
if subnets:
|
||||
# create new subnet entries
|
||||
for _, swidth, sexclude, snet \
|
||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||
for _, swidth, sexclude, snet in sorted(subnets,
|
||||
key=lambda s: s[1],
|
||||
reverse=True):
|
||||
if sexclude:
|
||||
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
||||
else:
|
||||
|
@ -50,6 +50,18 @@ class Method(BaseMethod):
|
||||
_ipt('-I', 'OUTPUT', '1', *args)
|
||||
_ipt('-I', 'PREROUTING', '1', *args)
|
||||
|
||||
# Firstly we always skip all LOCAL addtrype address, i.e. avoid
|
||||
# tunnelling the traffic designated to all local TCP/IP addresses.
|
||||
_ipt('-A', chain, '-j', 'RETURN',
|
||||
'-m', 'addrtype',
|
||||
'--dst-type', 'LOCAL',
|
||||
'!', '-p', 'udp')
|
||||
# Skip LOCAL traffic if it's not DNS.
|
||||
_ipt('-A', chain, '-j', 'RETURN',
|
||||
'-m', 'addrtype',
|
||||
'--dst-type', 'LOCAL',
|
||||
'-p', 'udp', '!', '--dport', '53')
|
||||
|
||||
# create new subnet entries.
|
||||
for _, swidth, sexclude, snet, fport, lport \
|
||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||
|
@ -1,6 +1,6 @@
|
||||
import socket
|
||||
from sshuttle.firewall import subnet_weight
|
||||
from sshuttle.linux import nft, nft_get_handle, nonfatal
|
||||
from sshuttle.linux import nft, nonfatal
|
||||
from sshuttle.methods import BaseMethod
|
||||
|
||||
|
||||
@ -16,19 +16,17 @@ class Method(BaseMethod):
|
||||
if udp:
|
||||
raise Exception("UDP not supported by nft")
|
||||
|
||||
table = "nat"
|
||||
table = 'sshuttle-%s' % port
|
||||
|
||||
def _nft(action, *args):
|
||||
return nft(family, table, action, *args)
|
||||
|
||||
chain = 'sshuttle-%s' % port
|
||||
chain = table
|
||||
|
||||
# basic cleanup/setup of chains
|
||||
_nft('add table', '')
|
||||
_nft('add chain', 'prerouting',
|
||||
'{ type nat hook prerouting priority -100; policy accept; }')
|
||||
_nft('add chain', 'postrouting',
|
||||
'{ type nat hook postrouting priority 100; policy accept; }')
|
||||
_nft('add chain', 'output',
|
||||
'{ type nat hook output priority -100; policy accept; }')
|
||||
_nft('add chain', chain)
|
||||
@ -40,8 +38,12 @@ class Method(BaseMethod):
|
||||
for _, swidth, sexclude, snet, fport, lport \
|
||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||
tcp_ports = ('ip', 'protocol', 'tcp')
|
||||
if fport:
|
||||
tcp_ports = tcp_ports + ('dport { %d-%d }' % (fport, lport))
|
||||
if fport and fport != lport:
|
||||
tcp_ports = \
|
||||
tcp_ports + \
|
||||
('tcp', 'dport', '{ %d-%d }' % (fport, lport))
|
||||
elif fport and fport == lport:
|
||||
tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport))
|
||||
|
||||
if sexclude:
|
||||
_nft('add rule', chain, *(tcp_ports + (
|
||||
@ -65,16 +67,10 @@ class Method(BaseMethod):
|
||||
if udp:
|
||||
raise Exception("UDP not supported by nft method_name")
|
||||
|
||||
table = "nat"
|
||||
table = 'sshuttle-%s' % port
|
||||
|
||||
def _nft(action, *args):
|
||||
return nft(family, table, action, *args)
|
||||
|
||||
chain = 'sshuttle-%s' % port
|
||||
|
||||
# basic cleanup/setup of chains
|
||||
handle = nft_get_handle('chain ip nat output', chain)
|
||||
nonfatal(_nft, 'delete rule', 'output', handle)
|
||||
handle = nft_get_handle('chain ip nat prerouting', chain)
|
||||
nonfatal(_nft, 'delete rule', 'prerouting', handle)
|
||||
nonfatal(_nft, 'delete chain', chain)
|
||||
nonfatal(_nft, 'delete table', '')
|
||||
|
@ -3,6 +3,7 @@ import sys
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import errno
|
||||
import struct
|
||||
import subprocess as ssubprocess
|
||||
import shlex
|
||||
@ -35,11 +36,11 @@ class Generic(object):
|
||||
|
||||
class pf_addr(Structure):
|
||||
class _pfa(Union):
|
||||
_fields_ = [("v4", c_uint32), # struct in_addr
|
||||
("v6", c_uint32 * 4), # struct in6_addr
|
||||
("addr8", c_uint8 * 16),
|
||||
("addr16", c_uint16 * 8),
|
||||
("addr32", c_uint32 * 4)]
|
||||
_fields_ = [("v4", c_uint32), # struct in_addr
|
||||
("v6", c_uint32 * 4), # struct in6_addr
|
||||
("addr8", c_uint8 * 16),
|
||||
("addr16", c_uint16 * 8),
|
||||
("addr32", c_uint32 * 4)]
|
||||
|
||||
_fields_ = [("pfa", _pfa)]
|
||||
_anonymous_ = ("pfa",)
|
||||
@ -120,16 +121,18 @@ class Generic(object):
|
||||
pr = self.pfioc_rule()
|
||||
|
||||
memmove(addressof(pr) + self.ANCHOR_CALL_OFFSET, name,
|
||||
min(self.MAXPATHLEN, len(name))) # anchor_call = name
|
||||
min(self.MAXPATHLEN, len(name))) # anchor_call = name
|
||||
memmove(addressof(pr) + self.RULE_ACTION_OFFSET,
|
||||
struct.pack('I', kind), 4) # rule.action = kind
|
||||
struct.pack('I', kind), 4) # rule.action = kind
|
||||
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
|
||||
'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET,
|
||||
struct.pack('I', self.PF_CHANGE_GET_TICKET),
|
||||
4) # action = PF_CHANGE_GET_TICKET
|
||||
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
|
||||
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
|
||||
'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET,
|
||||
struct.pack('I', self.PF_CHANGE_ADD_TAIL),
|
||||
4) # action = PF_CHANGE_ADD_TAIL
|
||||
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
|
||||
|
||||
@staticmethod
|
||||
@ -151,7 +154,6 @@ class Generic(object):
|
||||
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
|
||||
|
||||
|
||||
|
||||
class FreeBsd(Generic):
|
||||
RULE_ACTION_OFFSET = 2968
|
||||
|
||||
@ -178,6 +180,7 @@ class FreeBsd(Generic):
|
||||
|
||||
def enable(self):
|
||||
returncode = ssubprocess.call(['kldload', 'pf'])
|
||||
# No env: No output.
|
||||
super(FreeBsd, self).enable()
|
||||
if returncode == 0:
|
||||
_pf_context['loaded_by_sshuttle'] = True
|
||||
@ -187,6 +190,7 @@ class FreeBsd(Generic):
|
||||
if _pf_context['loaded_by_sshuttle'] and \
|
||||
_pf_context['started_by_sshuttle'] == 0:
|
||||
ssubprocess.call(['kldunload', 'pf'])
|
||||
# No env: No output.
|
||||
|
||||
def add_anchors(self, anchor):
|
||||
status = pfctl('-s all')[0]
|
||||
@ -217,7 +221,7 @@ class FreeBsd(Generic):
|
||||
b'pass out route-to lo0 %s proto tcp '
|
||||
b'to %s keep state' % (inet_version, subnet)
|
||||
if not exclude else
|
||||
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
|
||||
b'pass out %s proto tcp to %s' % (inet_version, subnet)
|
||||
for exclude, subnet in includes
|
||||
]
|
||||
|
||||
@ -261,7 +265,7 @@ class OpenBsd(Generic):
|
||||
("proto_variant", c_uint8),
|
||||
("direction", c_uint8)]
|
||||
|
||||
self.pfioc_rule = c_char * 3400
|
||||
self.pfioc_rule = c_char * 3424
|
||||
self.pfioc_natlook = pfioc_natlook
|
||||
super(OpenBsd, self).__init__()
|
||||
|
||||
@ -287,7 +291,7 @@ class OpenBsd(Generic):
|
||||
b'pass out %s proto tcp to %s '
|
||||
b'route-to lo0 keep state' % (inet_version, subnet)
|
||||
if not exclude else
|
||||
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
|
||||
b'pass out %s proto tcp to %s' % (inet_version, subnet)
|
||||
for exclude, subnet in includes
|
||||
]
|
||||
|
||||
@ -420,7 +424,13 @@ class Method(BaseMethod):
|
||||
def get_tcp_dstip(self, sock):
|
||||
pfile = self.firewall.pfile
|
||||
|
||||
peer = sock.getpeername()
|
||||
try:
|
||||
peer = sock.getpeername()
|
||||
except socket.error:
|
||||
_, e = sys.exc_info()[:2]
|
||||
if e.args[0] == errno.EINVAL:
|
||||
return sock.getsockname()
|
||||
|
||||
proxy = sock.getsockname()
|
||||
|
||||
argv = (sock.family, socket.IPPROTO_TCP,
|
||||
@ -452,7 +462,7 @@ class Method(BaseMethod):
|
||||
# exclusion first; the table will ignore the second, opposite
|
||||
# definition
|
||||
for _, swidth, sexclude, snet, fport, lport \
|
||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||
in sorted(subnets, key=subnet_weight):
|
||||
includes.append((sexclude, b"%s/%d%s" % (
|
||||
snet.encode("ASCII"),
|
||||
swidth,
|
||||
|
@ -169,7 +169,6 @@ class Method(BaseMethod):
|
||||
return proto + ('--dport', '%d:%d' % (fport, lport)) \
|
||||
if fport else proto
|
||||
|
||||
|
||||
mark_chain = 'sshuttle-m-%s' % port
|
||||
tproxy_chain = 'sshuttle-t-%s' % port
|
||||
divert_chain = 'sshuttle-d-%s' % port
|
||||
@ -245,7 +244,8 @@ class Method(BaseMethod):
|
||||
else:
|
||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'udp', '-p', 'udp')
|
||||
'-m', 'udp',
|
||||
*udp_ports)
|
||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import socket
|
||||
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
|
||||
|
||||
from sshuttle import __version__
|
||||
|
||||
|
||||
@ -31,7 +32,7 @@ def parse_subnetport(s):
|
||||
if s.count(':') > 1:
|
||||
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
|
||||
else:
|
||||
rx = r'([\w\.]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
|
||||
rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
|
||||
|
||||
m = re.match(rx, s)
|
||||
if not m:
|
||||
@ -62,7 +63,7 @@ def parse_ipport(s):
|
||||
elif ']' in s:
|
||||
rx = r'(?:\[([^]]+)])(?::(\d+))?$'
|
||||
else:
|
||||
rx = r'([\w\.]+)(?::(\d+))?$'
|
||||
rx = r'([\w\.\-]+)(?::(\d+))?$'
|
||||
|
||||
m = re.match(rx, s)
|
||||
if not m:
|
||||
@ -176,9 +177,9 @@ parser.add_argument(
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--remote",
|
||||
metavar="[USERNAME@]ADDR[:PORT]",
|
||||
metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
|
||||
help="""
|
||||
ssh hostname (and optional username) of remote %(prog)s server
|
||||
ssh hostname (and optional username and password) of remote %(prog)s server
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -242,6 +243,16 @@ parser.add_argument(
|
||||
sacrifice latency to improve bandwidth benchmarks
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--latency-buffer-size",
|
||||
metavar="SIZE",
|
||||
type=int,
|
||||
default=32768,
|
||||
dest="latency_buffer_size",
|
||||
help="""
|
||||
size of latency control buffer
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wrap",
|
||||
metavar="NUM",
|
||||
@ -310,3 +321,42 @@ parser.add_argument(
|
||||
(internal use only)
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers",
|
||||
action="store_true",
|
||||
help="""
|
||||
Add sshuttle to the sudoers for this user
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers-no-modify",
|
||||
action="store_true",
|
||||
help="""
|
||||
Prints the sudoers config to STDOUT and DOES NOT modify anything.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers-user",
|
||||
default="",
|
||||
help="""
|
||||
Set the user name or group with %%group_name for passwordless operation.
|
||||
Default is the current user.set ALL for all users. Only works with
|
||||
--sudoers or --sudoers-no-modify option.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers-filename",
|
||||
default="sshuttle_auto",
|
||||
help="""
|
||||
Set the file name for the sudoers.d file to be added. Default is
|
||||
"sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-sudo-pythonpath",
|
||||
action="store_false",
|
||||
dest="sudo_pythonpath",
|
||||
help="""
|
||||
do not set PYTHONPATH when invoking sudo
|
||||
"""
|
||||
)
|
||||
|
@ -1,7 +1,9 @@
|
||||
import socket
|
||||
import os
|
||||
|
||||
from sshuttle.helpers import debug1
|
||||
|
||||
|
||||
def _notify(message):
|
||||
addr = os.environ.get("NOTIFY_SOCKET", None)
|
||||
|
||||
@ -27,14 +29,18 @@ def _notify(message):
|
||||
debug1("Error notifying systemd: %s\n" % e)
|
||||
return False
|
||||
|
||||
|
||||
def send(*messages):
|
||||
return _notify(b'\n'.join(messages))
|
||||
|
||||
|
||||
def ready():
|
||||
return b"READY=1"
|
||||
|
||||
|
||||
def stop():
|
||||
return b"STOPPING=1"
|
||||
|
||||
|
||||
def status(message):
|
||||
return b"STATUS=%s" % message.encode('utf8')
|
||||
|
@ -6,6 +6,7 @@ import time
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
from shutil import which
|
||||
|
||||
import sshuttle.ssnet as ssnet
|
||||
import sshuttle.helpers as helpers
|
||||
@ -15,17 +16,12 @@ from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
||||
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
|
||||
resolvconf_random_nameserver
|
||||
|
||||
try:
|
||||
from shutil import which
|
||||
except ImportError:
|
||||
from distutils.spawn import find_executable as which
|
||||
|
||||
|
||||
def _ipmatch(ipstr):
|
||||
# FIXME: IPv4 only
|
||||
if ipstr == 'default':
|
||||
ipstr = '0.0.0.0/0'
|
||||
m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||
if m:
|
||||
g = m.groups()
|
||||
ips = g[0]
|
||||
@ -279,7 +275,7 @@ class UdpProxy(Handler):
|
||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||
|
||||
|
||||
def main(latency_control, auto_hosts, to_nameserver):
|
||||
def main(latency_control, auto_hosts, to_nameserver, auto_nets):
|
||||
debug1('Starting server with Python version %s\n'
|
||||
% platform.python_version())
|
||||
|
||||
@ -289,21 +285,23 @@ def main(latency_control, auto_hosts, to_nameserver):
|
||||
helpers.logprefix = 'server: '
|
||||
debug1('latency control setting = %r\n' % latency_control)
|
||||
|
||||
routes = list(list_routes())
|
||||
debug1('available routes:\n')
|
||||
for r in routes:
|
||||
debug1(' %d/%s/%d\n' % r)
|
||||
|
||||
# synchronization header
|
||||
sys.stdout.write('\0\0SSHUTTLE0001')
|
||||
sys.stdout.flush()
|
||||
|
||||
handlers = []
|
||||
mux = Mux(socket.fromfd(sys.stdin.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM),
|
||||
socket.fromfd(sys.stdout.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM))
|
||||
mux = Mux(sys.stdin, sys.stdout)
|
||||
handlers.append(mux)
|
||||
|
||||
debug1('auto-nets:' + str(auto_nets) + '\n')
|
||||
if auto_nets:
|
||||
routes = list(list_routes())
|
||||
debug1('available routes:\n')
|
||||
for r in routes:
|
||||
debug1(' %d/%s/%d\n' % r)
|
||||
else:
|
||||
routes = []
|
||||
|
||||
routepkt = ''
|
||||
for r in routes:
|
||||
routepkt += '%d,%s,%d\n' % r
|
||||
|
110
sshuttle/ssh.py
110
sshuttle/ssh.py
@ -6,16 +6,13 @@ import zlib
|
||||
import imp
|
||||
import subprocess as ssubprocess
|
||||
import shlex
|
||||
from shlex import quote
|
||||
import ipaddress
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import sshuttle.helpers as helpers
|
||||
from sshuttle.helpers import debug2
|
||||
|
||||
try:
|
||||
# Python >= 3.5
|
||||
from shlex import quote
|
||||
except ImportError:
|
||||
# Python 2.x
|
||||
from pipes import quote
|
||||
|
||||
|
||||
def readfile(name):
|
||||
tokens = name.split(".")
|
||||
@ -60,29 +57,64 @@ def empackage(z, name, data=None):
|
||||
return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content)
|
||||
|
||||
|
||||
def parse_hostport(rhostport):
|
||||
"""
|
||||
parses the given rhostport variable, looking like this:
|
||||
|
||||
[username[:password]@]host[:port]
|
||||
|
||||
if only host is given, can be a hostname, IPv4/v6 address or a ssh alias
|
||||
from ~/.ssh/config
|
||||
|
||||
and returns a tuple (username, password, port, host)
|
||||
"""
|
||||
# default port for SSH is TCP port 22
|
||||
port = 22
|
||||
username = None
|
||||
password = None
|
||||
host = rhostport
|
||||
|
||||
if "@" in host:
|
||||
# split username (and possible password) from the host[:port]
|
||||
username, host = host.split("@")
|
||||
# Fix #410 bad username error detect
|
||||
# username cannot contain an @ sign in this scenario
|
||||
if ":" in username:
|
||||
# this will even allow for the username to be empty
|
||||
username, password = username.split(":")
|
||||
|
||||
if ":" in host:
|
||||
# IPv6 address and/or got a port specified
|
||||
|
||||
# If it is an IPv6 adress with port specification,
|
||||
# then it will look like: [::1]:22
|
||||
|
||||
try:
|
||||
# try to parse host as an IP adress,
|
||||
# if that works it is an IPv6 address
|
||||
host = ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
# if that fails parse as URL to get the port
|
||||
parsed = urlparse('//{}'.format(host))
|
||||
try:
|
||||
host = ipaddress.ip_address(parsed.hostname)
|
||||
except ValueError:
|
||||
# else if both fails, we have a hostname with port
|
||||
host = parsed.hostname
|
||||
port = parsed.port
|
||||
|
||||
if password is None or len(password) == 0:
|
||||
password = None
|
||||
|
||||
return username, password, port, host
|
||||
|
||||
|
||||
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
portl = []
|
||||
|
||||
if re.sub(r'.*@', '', rhostport or '').count(':') > 1:
|
||||
if rhostport.count(']') or rhostport.count('['):
|
||||
result = rhostport.split(']')
|
||||
rhost = result[0].strip('[')
|
||||
if len(result) > 1:
|
||||
result[1] = result[1].strip(':')
|
||||
if result[1] != '':
|
||||
portl = ['-p', str(int(result[1]))]
|
||||
# can't disambiguate IPv6 colons and a port number. pass the hostname
|
||||
# through.
|
||||
else:
|
||||
rhost = rhostport
|
||||
else: # IPv4
|
||||
l = (rhostport or '').rsplit(':', 1)
|
||||
rhost = l[0]
|
||||
if len(l) > 1:
|
||||
portl = ['-p', str(int(l[1]))]
|
||||
|
||||
if rhost == '-':
|
||||
rhost = None
|
||||
username, password, port, host = parse_hostport(rhostport)
|
||||
if username:
|
||||
rhost = "{}@{}".format(username, host)
|
||||
else:
|
||||
rhost = host
|
||||
|
||||
z = zlib.compressobj(1)
|
||||
content = readfile('sshuttle.assembler')
|
||||
@ -116,12 +148,20 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
if python:
|
||||
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
||||
else:
|
||||
pycmd = ("P=python3; $P -V 2>/dev/null || P=python; "
|
||||
"exec \"$P\" -c %s") % quote(pyscript)
|
||||
pycmd = ("exec /bin/sh -c %s" % quote(pycmd))
|
||||
argv = (sshl +
|
||||
portl +
|
||||
[rhost, '--', pycmd])
|
||||
pycmd = ("P=python3; $P -V 2>%s || P=python; "
|
||||
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript))
|
||||
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
|
||||
|
||||
if password is not None:
|
||||
os.environ['SSHPASS'] = str(password)
|
||||
argv = (["sshpass", "-e"] + sshl +
|
||||
["-p", str(port)] +
|
||||
[rhost, '--', pycmd])
|
||||
|
||||
else:
|
||||
argv = (sshl +
|
||||
["-p", str(port)] +
|
||||
[rhost, '--', pycmd])
|
||||
(s1, s2) = socket.socketpair()
|
||||
|
||||
def setup():
|
||||
|
@ -4,9 +4,11 @@ import socket
|
||||
import errno
|
||||
import select
|
||||
import os
|
||||
from sshuttle.helpers import b, binary_type, log, debug1, debug2, debug3, Fatal
|
||||
|
||||
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
|
||||
|
||||
MAX_CHANNEL = 65535
|
||||
LATENCY_BUFFER_SIZE = 32768
|
||||
|
||||
# these don't exist in the socket module in python 2.3!
|
||||
SHUT_RD = 0
|
||||
@ -55,17 +57,18 @@ cmd_to_name = {
|
||||
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||
errno.EHOSTDOWN, errno.ENETDOWN,
|
||||
errno.ENETUNREACH]
|
||||
errno.ENETUNREACH, errno.ECONNABORTED,
|
||||
errno.ECONNRESET]
|
||||
|
||||
|
||||
def _add(l, elem):
|
||||
if elem not in l:
|
||||
l.append(elem)
|
||||
def _add(socks, elem):
|
||||
if elem not in socks:
|
||||
socks.append(elem)
|
||||
|
||||
|
||||
def _fds(l):
|
||||
def _fds(socks):
|
||||
out = []
|
||||
for i in l:
|
||||
for i in socks:
|
||||
try:
|
||||
out.append(i.fileno())
|
||||
except AttributeError:
|
||||
@ -93,8 +96,12 @@ def _try_peername(sock):
|
||||
return '%s:%s' % (pn[0], pn[1])
|
||||
except socket.error:
|
||||
_, e = sys.exc_info()[:2]
|
||||
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
||||
if e.args[0] == errno.EINVAL:
|
||||
pass
|
||||
elif e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
||||
raise
|
||||
except AttributeError:
|
||||
pass
|
||||
return 'unknown'
|
||||
|
||||
|
||||
@ -332,10 +339,10 @@ class Proxy(Handler):
|
||||
|
||||
class Mux(Handler):
|
||||
|
||||
def __init__(self, rsock, wsock):
|
||||
Handler.__init__(self, [rsock, wsock])
|
||||
self.rsock = rsock
|
||||
self.wsock = wsock
|
||||
def __init__(self, rfile, wfile):
|
||||
Handler.__init__(self, [rfile, wfile])
|
||||
self.rfile = rfile
|
||||
self.wfile = wfile
|
||||
self.new_channel = self.got_dns_req = self.got_routes = None
|
||||
self.got_udp_open = self.got_udp_data = self.got_udp_close = None
|
||||
self.got_host_req = self.got_host_list = None
|
||||
@ -364,7 +371,7 @@ class Mux(Handler):
|
||||
return total
|
||||
|
||||
def check_fullness(self):
|
||||
if self.fullness > 32768:
|
||||
if self.fullness > LATENCY_BUFFER_SIZE:
|
||||
if not self.too_full:
|
||||
self.send(0, CMD_PING, b('rttest'))
|
||||
self.too_full = True
|
||||
@ -375,7 +382,7 @@ class Mux(Handler):
|
||||
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
||||
|
||||
def send(self, channel, cmd, data):
|
||||
assert isinstance(data, binary_type)
|
||||
assert isinstance(data, bytes)
|
||||
assert len(data) <= 65535
|
||||
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
|
||||
+ data
|
||||
@ -432,9 +439,9 @@ class Mux(Handler):
|
||||
callback(cmd, data)
|
||||
|
||||
def flush(self):
|
||||
self.wsock.setblocking(False)
|
||||
os.set_blocking(self.wfile.fileno(), False)
|
||||
if self.outbuf and self.outbuf[0]:
|
||||
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
|
||||
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
|
||||
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
|
||||
if wrote:
|
||||
self.outbuf[0] = self.outbuf[0][wrote:]
|
||||
@ -442,9 +449,9 @@ class Mux(Handler):
|
||||
self.outbuf[0:1] = []
|
||||
|
||||
def fill(self):
|
||||
self.rsock.setblocking(False)
|
||||
os.set_blocking(self.rfile.fileno(), False)
|
||||
try:
|
||||
read = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
||||
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
|
||||
except OSError:
|
||||
_, e = sys.exc_info()[:2]
|
||||
raise Fatal('other end: %r' % e)
|
||||
@ -474,22 +481,22 @@ class Mux(Handler):
|
||||
break
|
||||
|
||||
def pre_select(self, r, w, x):
|
||||
_add(r, self.rsock)
|
||||
_add(r, self.rfile)
|
||||
if self.outbuf:
|
||||
_add(w, self.wsock)
|
||||
_add(w, self.wfile)
|
||||
|
||||
def callback(self, sock):
|
||||
(r, w, _) = select.select([self.rsock], [self.wsock], [], 0)
|
||||
if self.rsock in r:
|
||||
(r, w, _) = select.select([self.rfile], [self.wfile], [], 0)
|
||||
if self.rfile in r:
|
||||
self.handle()
|
||||
if self.outbuf and self.wsock in w:
|
||||
if self.outbuf and self.wfile in w:
|
||||
self.flush()
|
||||
|
||||
|
||||
class MuxWrapper(SockWrapper):
|
||||
|
||||
def __init__(self, mux, channel):
|
||||
SockWrapper.__init__(self, mux.rsock, mux.wsock)
|
||||
SockWrapper.__init__(self, mux.rfile, mux.wfile)
|
||||
self.mux = mux
|
||||
self.channel = channel
|
||||
self.mux.channels[channel] = self.got_packet
|
||||
|
@ -8,12 +8,24 @@ _p = None
|
||||
|
||||
def start_syslog():
|
||||
global _p
|
||||
_p = ssubprocess.Popen(['logger',
|
||||
'-p', 'daemon.notice',
|
||||
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
_p = ssubprocess.Popen(
|
||||
['logger', '-p', 'daemon.notice', '-t', 'sshuttle'],
|
||||
stdin=ssubprocess.PIPE,
|
||||
stdout=devnull,
|
||||
stderr=devnull
|
||||
)
|
||||
|
||||
|
||||
def close_stdin():
|
||||
sys.stdin.close()
|
||||
|
||||
|
||||
def stdout_to_syslog():
|
||||
sys.stdout.flush()
|
||||
os.dup2(_p.stdin.fileno(), sys.stdout.fileno())
|
||||
|
||||
|
||||
def stderr_to_syslog():
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os.dup2(_p.stdin.fileno(), 2)
|
||||
os.dup2(_p.stdin.fileno(), sys.stderr.fileno())
|
||||
|
64
sshuttle/sudoers.py
Normal file
64
sshuttle/sudoers.py
Normal file
@ -0,0 +1,64 @@
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
from uuid import uuid4
|
||||
from subprocess import Popen, PIPE
|
||||
from sshuttle.helpers import log, debug1
|
||||
from distutils import spawn
|
||||
|
||||
path_to_sshuttle = sys.argv[0]
|
||||
path_to_dist_packages = os.path.dirname(os.path.abspath(__file__))[:-9]
|
||||
|
||||
# randomize command alias to avoid collisions
|
||||
command_alias = 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()}
|
||||
|
||||
# Template for the sudoers file
|
||||
template = '''
|
||||
Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
|
||||
|
||||
%(user_name)s ALL=NOPASSWD: %(ca)s
|
||||
'''
|
||||
|
||||
|
||||
def build_config(user_name):
|
||||
content = template % {
|
||||
'ca': command_alias,
|
||||
'dist_packages': path_to_dist_packages,
|
||||
'py': sys.executable,
|
||||
'path': path_to_sshuttle,
|
||||
'user_name': user_name,
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def save_config(content, file_name):
|
||||
process = Popen([
|
||||
'/usr/bin/sudo',
|
||||
spawn.find_executable('sudoers-add'),
|
||||
file_name,
|
||||
], stdout=PIPE, stdin=PIPE)
|
||||
|
||||
process.stdin.write(content.encode())
|
||||
|
||||
streamdata = process.communicate()[0]
|
||||
returncode = process.returncode
|
||||
|
||||
if returncode:
|
||||
log('Failed updating sudoers file.\n')
|
||||
debug1(streamdata)
|
||||
exit(returncode)
|
||||
else:
|
||||
log('Success, sudoers file update.\n')
|
||||
exit(0)
|
||||
|
||||
|
||||
def sudoers(user_name=None, no_modify=None, file_name=None):
|
||||
user_name = user_name or getpass.getuser()
|
||||
content = build_config(user_name)
|
||||
|
||||
if no_modify:
|
||||
sys.stdout.write(content)
|
||||
exit(0)
|
||||
else:
|
||||
save_config(content, file_name)
|
@ -1,7 +1,7 @@
|
||||
from mock import Mock, patch, call
|
||||
import io
|
||||
from socket import AF_INET, AF_INET6
|
||||
|
||||
from mock import Mock, patch, call
|
||||
import sshuttle.firewall
|
||||
|
||||
|
||||
@ -85,8 +85,9 @@ def test_subnet_weight():
|
||||
(AF_INET, 0, 1, '0.0.0.0', 0, 0)
|
||||
]
|
||||
|
||||
assert subnets_sorted == \
|
||||
sorted(subnets, key=sshuttle.firewall.subnet_weight, reverse=True)
|
||||
assert subnets_sorted == sorted(subnets,
|
||||
key=sshuttle.firewall.subnet_weight,
|
||||
reverse=True)
|
||||
|
||||
|
||||
@patch('sshuttle.firewall.rewrite_etc_hosts')
|
@ -1,10 +1,9 @@
|
||||
from mock import patch, call
|
||||
import sys
|
||||
import io
|
||||
import socket
|
||||
from socket import AF_INET, AF_INET6
|
||||
import errno
|
||||
|
||||
from mock import patch, call
|
||||
import sshuttle.helpers
|
||||
|
||||
|
||||
@ -193,9 +192,5 @@ def test_family_ip_tuple():
|
||||
def test_family_to_string():
|
||||
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
|
||||
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
|
||||
if sys.version_info < (3, 0):
|
||||
expected = "1"
|
||||
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1"
|
||||
else:
|
||||
expected = 'AddressFamily.AF_UNIX'
|
||||
expected = 'AddressFamily.AF_UNIX'
|
||||
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected
|
@ -1,9 +1,9 @@
|
||||
import pytest
|
||||
from mock import Mock, patch, call
|
||||
import socket
|
||||
from socket import AF_INET, AF_INET6
|
||||
import struct
|
||||
|
||||
import pytest
|
||||
from mock import Mock, patch, call
|
||||
from sshuttle.helpers import Fatal
|
||||
from sshuttle.methods import get_method
|
||||
|
||||
@ -124,7 +124,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||
]
|
||||
assert mock_ipt_ttl.mock_calls == [
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
|
||||
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
|
||||
'--to-ports', '1025'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||
'--dest', u'1.2.3.33/32', '-p', 'udp',
|
||||
@ -140,7 +140,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||
call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
|
||||
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
|
||||
'-m', 'addrtype', '--dst-type', 'LOCAL',
|
||||
'!', '-p', 'udp'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||
'-m', 'addrtype', '--dst-type', 'LOCAL',
|
||||
'-p', 'udp', '!', '--dport', '53'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
|
||||
]
|
||||
mock_ipt_chain_exists.reset_mock()
|
||||
mock_ipt_ttl.reset_mock()
|
@ -1,8 +1,8 @@
|
||||
import pytest
|
||||
from mock import Mock, patch, call, ANY
|
||||
import socket
|
||||
from socket import AF_INET, AF_INET6
|
||||
|
||||
import pytest
|
||||
from mock import Mock, patch, call, ANY
|
||||
from sshuttle.methods import get_method
|
||||
from sshuttle.helpers import Fatal
|
||||
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
|
||||
@ -205,10 +205,10 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n'
|
||||
b'rdr pass on lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 -> ::1 port 1026\n'
|
||||
b'pass out quick inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
|
||||
b'pass out inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-E'),
|
||||
@ -257,8 +257,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'-> 127.0.0.1 port 1025\n'
|
||||
b'rdr pass on lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
||||
b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
|
||||
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-E'),
|
||||
@ -308,10 +308,10 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n'
|
||||
b'rdr pass on lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 -> ::1 port 1026\n'
|
||||
b'pass out quick inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
|
||||
b'pass out inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-e'),
|
||||
@ -359,8 +359,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
||||
b'to 1.2.3.0/24 -> 127.0.0.1 port 1025\n'
|
||||
b'rdr pass on lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
||||
b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
|
||||
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-e'),
|
||||
@ -403,8 +403,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
None)
|
||||
|
||||
assert mock_ioctl.mock_calls == [
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
]
|
||||
assert mock_pfctl.mock_calls == [
|
||||
call('-s Interfaces -i lo -v'),
|
||||
@ -416,10 +416,10 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'port 8000:9000 divert-to ::1 port 1024\n'
|
||||
b'pass in on lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 rdr-to ::1 port 1026\n'
|
||||
b'pass out quick inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out inet6 proto tcp to 2404:6800:4004:80c::/64 '
|
||||
b'port 8000:9000 route-to lo0 keep state\n'
|
||||
b'pass out inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out inet6 proto udp to '
|
||||
b'<dns_servers> port 53 route-to lo0 keep state\n'),
|
||||
call('-e'),
|
||||
@ -451,8 +451,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
False,
|
||||
None)
|
||||
assert mock_ioctl.mock_calls == [
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
]
|
||||
assert mock_pfctl.mock_calls == [
|
||||
call('-s Interfaces -i lo -v'),
|
||||
@ -464,8 +464,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'127.0.0.1 port 1025\n'
|
||||
b'pass in on lo0 inet proto udp to '
|
||||
b'<dns_servers> port 53 rdr-to 127.0.0.1 port 1027\n'
|
||||
b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out inet proto tcp to 1.2.3.0/24 route-to lo0 keep state\n'
|
||||
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out inet proto udp to '
|
||||
b'<dns_servers> port 53 route-to lo0 keep state\n'),
|
||||
call('-e'),
|
@ -132,7 +132,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||
call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'),
|
||||
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
||||
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||
'sshuttle-t-1024'),
|
||||
'sshuttle-t-1024'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
|
||||
'--set-mark', '1'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
||||
@ -168,7 +168,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||
'--on-port', '1024'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
||||
'-m', 'udp', '-p', 'udp'),
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
|
||||
@ -232,7 +232,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||
call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
||||
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||
'sshuttle-t-1025'),
|
||||
'sshuttle-t-1025'),
|
||||
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
|
||||
'-j', 'MARK', '--set-mark', '1'),
|
||||
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
|
@ -1,8 +1,10 @@
|
||||
import socket
|
||||
import pytest
|
||||
import sshuttle.options
|
||||
from argparse import ArgumentTypeError as Fatal
|
||||
|
||||
import pytest
|
||||
|
||||
import sshuttle.options
|
||||
|
||||
_ip4_reprs = {
|
||||
'0.0.0.0': '0.0.0.0',
|
||||
'255.255.255.255': '255.255.255.255',
|
||||
@ -25,6 +27,7 @@ _ip6_reprs = {
|
||||
|
||||
_ip6_swidths = (48, 64, 96, 115, 128)
|
||||
|
||||
|
||||
def test_parse_subnetport_ip4():
|
||||
for ip_repr, ip in _ip4_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport(ip_repr) \
|
||||
@ -41,7 +44,7 @@ def test_parse_subnetport_ip4_with_mask():
|
||||
'/'.join((ip_repr, str(swidth)))
|
||||
) == (socket.AF_INET, ip, swidth, 0, 0)
|
||||
assert sshuttle.options.parse_subnetport('0/0') \
|
||||
== (socket.AF_INET, '0.0.0.0', 0, 0, 0)
|
||||
== (socket.AF_INET, '0.0.0.0', 0, 0, 0)
|
||||
with pytest.raises(Fatal) as excinfo:
|
||||
sshuttle.options.parse_subnetport('10.0.0.0/33')
|
||||
assert str(excinfo.value) == 'width 33 is not between 0 and 32'
|
||||
@ -50,17 +53,17 @@ def test_parse_subnetport_ip4_with_mask():
|
||||
def test_parse_subnetport_ip4_with_port():
|
||||
for ip_repr, ip in _ip4_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \
|
||||
== (socket.AF_INET, ip, 32, 80, 80)
|
||||
== (socket.AF_INET, ip, 32, 80, 80)
|
||||
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\
|
||||
== (socket.AF_INET, ip, 32, 80, 90)
|
||||
== (socket.AF_INET, ip, 32, 80, 90)
|
||||
|
||||
|
||||
def test_parse_subnetport_ip4_with_mask_and_port():
|
||||
for ip_repr, ip in _ip4_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \
|
||||
== (socket.AF_INET, ip, 32, 80, 80)
|
||||
== (socket.AF_INET, ip, 32, 80, 80)
|
||||
assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \
|
||||
== (socket.AF_INET, ip, 16, 80, 90)
|
||||
== (socket.AF_INET, ip, 16, 80, 90)
|
||||
|
||||
|
||||
def test_parse_subnetport_ip6():
|
||||
@ -76,7 +79,7 @@ def test_parse_subnetport_ip6_with_mask():
|
||||
'/'.join((ip_repr, str(swidth)))
|
||||
) == (socket.AF_INET6, ip, swidth, 0, 0)
|
||||
assert sshuttle.options.parse_subnetport('::/0') \
|
||||
== (socket.AF_INET6, '::', 0, 0, 0)
|
||||
== (socket.AF_INET6, '::', 0, 0, 0)
|
||||
with pytest.raises(Fatal) as excinfo:
|
||||
sshuttle.options.parse_subnetport('fc00::/129')
|
||||
assert str(excinfo.value) == 'width 129 is not between 0 and 128'
|
||||
@ -85,14 +88,14 @@ def test_parse_subnetport_ip6_with_mask():
|
||||
def test_parse_subnetport_ip6_with_port():
|
||||
for ip_repr, ip in _ip6_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \
|
||||
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \
|
||||
== (socket.AF_INET6, ip, 128, 80, 90)
|
||||
== (socket.AF_INET6, ip, 128, 80, 90)
|
||||
|
||||
|
||||
def test_parse_subnetport_ip6_with_mask_and_port():
|
||||
for ip_repr, ip in _ip6_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \
|
||||
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
|
||||
== (socket.AF_INET6, ip, 16, 80, 90)
|
||||
== (socket.AF_INET6, ip, 16, 80, 90)
|
@ -1,6 +1,7 @@
|
||||
from mock import Mock, patch, call
|
||||
import socket
|
||||
|
||||
from mock import Mock, patch, call
|
||||
|
||||
import sshuttle.sdnotify
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import io
|
||||
import socket
|
||||
import sshuttle.server
|
||||
|
||||
from mock import patch, Mock
|
||||
|
||||
import sshuttle.server
|
||||
|
||||
|
||||
def test__ipmatch():
|
||||
assert sshuttle.server._ipmatch("1.2.3.4") is not None
|
19
tox.ini
19
tox.ini
@ -1,21 +1,22 @@
|
||||
[tox]
|
||||
downloadcache = {toxworkdir}/cache/
|
||||
envlist =
|
||||
py27,
|
||||
py34,
|
||||
py35,
|
||||
py36,
|
||||
py37,
|
||||
py38,
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py26: python2.6
|
||||
py27: python2.7
|
||||
py34: python3.4
|
||||
py35: python3.5
|
||||
py36: python3.6
|
||||
py37: python3.7
|
||||
py38: python3.8
|
||||
commands =
|
||||
flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
|
||||
flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
py.test
|
||||
pip install -e .
|
||||
# actual flake8 test
|
||||
flake8 sshuttle tests
|
||||
# flake8 complexity warnings
|
||||
flake8 sshuttle tests --exit-zero --max-complexity=10
|
||||
pytest
|
||||
deps =
|
||||
-rrequirements-tests.txt
|
||||
|
Reference in New Issue
Block a user