mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 16:50:34 +02:00
Compare commits
150 Commits
Author | SHA1 | Date | |
---|---|---|---|
3d51bcba95 | |||
3331159821 | |||
d23a0fd2c5 | |||
164ceac198 | |||
ecc2d68a06 | |||
f1bae5ea04 | |||
be667c7854 | |||
952336f97b | |||
0890ebd383 | |||
d593e8c4f7 | |||
9429f387ea | |||
4e43af758d | |||
0ccd243a65 | |||
9e4822d7b7 | |||
80a822e079 | |||
8e826cfa7d | |||
286bd3fa80 | |||
ae8af71886 | |||
54b80e6ce2 | |||
b00f2e0a68 | |||
15a8752cc6 | |||
a2776cbbd9 | |||
44b772d049 | |||
ae1faa7fa1 | |||
175da40db7 | |||
e11db3980f | |||
d6f75fae25 | |||
354cbe6071 | |||
922d827948 | |||
e6f076e1a5 | |||
2665b67926 | |||
0cfee0ba84 | |||
f247853f39 | |||
7c1f2b08cf | |||
f65a51bbee | |||
b1ee4f1d65 | |||
4f20efd592 | |||
fa0de2d1b7 | |||
01415dc52e | |||
9257077b4e | |||
b896a4b7f3 | |||
fca9bd6478 | |||
42bb67b050 | |||
b7800dadda | |||
3de631abec | |||
35382a98a7 | |||
bebfa45626 | |||
387dd4775b | |||
f5cf79893a | |||
86a651099e | |||
a42f6a5e67 | |||
5c4c943db8 | |||
ccf1c9f478 | |||
9399cf10f6 | |||
b95c1f3357 | |||
5257d9fd1b | |||
ef244a5490 | |||
7fb0f0a81b | |||
e8653d444d | |||
83038f0472 | |||
9974ca17f8 | |||
e2f2fecc8a | |||
b59ce9c014 | |||
aa50f762cc | |||
57640ea2d0 | |||
e59cc3959c | |||
86d20da82a | |||
d3fae535cd | |||
7d44e890eb | |||
68a7766d24 | |||
09aab1a0fa | |||
bd00a530dd | |||
bc065e368d | |||
6ae0b51c61 | |||
d7e257642e | |||
bea5e1a1f4 | |||
99787c1459 | |||
ee02e1aaa2 | |||
c06581cf83 | |||
78b80709f2 | |||
d3f4889f21 | |||
560c6b4ce8 | |||
d21a322f05 | |||
3e308b4266 | |||
a3cbf0885f | |||
3f201095ea | |||
58c264ff1c | |||
1820264dd5 | |||
be847f5d81 | |||
d2836a082f | |||
8c5ffc9e72 | |||
843a729d64 | |||
851b26cb13 | |||
c026a92cad | |||
1dbec7252d | |||
a7df12cd68 | |||
bc54ffe398 | |||
31f059883c | |||
f376674941 | |||
41ca86dbf2 | |||
7760d63870 | |||
8b69c706c2 | |||
d289fb2e73 | |||
20c38afaba | |||
7aeb7929c5 | |||
30767135fb | |||
df383edaf6 | |||
88154f4f9c | |||
915497f73f | |||
652113818e | |||
cdfb4b7d71 | |||
45b788c908 | |||
f6176d3581 | |||
bc08198cae | |||
613412b3cd | |||
4f7706db59 | |||
7307636d35 | |||
1f5161e48c | |||
d505b08104 | |||
3e3608f572 | |||
51a1078371 | |||
f55983e485 | |||
e54747bfb0 | |||
d8acf15c1e | |||
bafc0a7038 | |||
4c11dd7183 | |||
4257c8d34e | |||
dc657a93f4 | |||
5e177d81bc | |||
7015d7a823 | |||
014a268b99 | |||
97076a9390 | |||
dd661e9728 | |||
d6d11b24c8 | |||
bb1363ec6b | |||
167a57e739 | |||
0e51da519f | |||
a22c453d5e | |||
f928f94093 | |||
127cac37ef | |||
d68f57b534 | |||
0933684d0f | |||
bf7b4a6c34 | |||
65d437a9f8 | |||
254b8e7987 | |||
97c25e988e | |||
b7730fc106 | |||
7fc33c0020 | |||
563f41478a | |||
da848b6dba |
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
12
.github/workflows/pythonpackage.yml
vendored
12
.github/workflows/pythonpackage.yml
vendored
@ -5,11 +5,11 @@ name: Python package
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master, tproxy_mark_param ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master, tproxy_mark_param ]
|
branches: [ master ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
branches: [ tproxy_mark_param ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -17,12 +17,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2.4.0
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2.3.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
15
.readthedocs.yaml
Normal file
15
.readthedocs.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-20.04
|
||||||
|
tools:
|
||||||
|
python: "3.9"
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: requirements.txt
|
||||||
|
- method: setuptools
|
||||||
|
path: .
|
@ -1,12 +1,9 @@
|
|||||||
==========
|
==========
|
||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
All notable changes to this project will be documented in this file. The format
|
Release notes now moved to https://github.com/sshuttle/sshuttle/releases/
|
||||||
is based on `Keep a Changelog`_ and this project
|
|
||||||
adheres to `Semantic Versioning`_.
|
|
||||||
|
|
||||||
.. _`Keep a Changelog`: http://keepachangelog.com/
|
These are the old release notes.
|
||||||
.. _`Semantic Versioning`: http://semver.org/
|
|
||||||
|
|
||||||
|
|
||||||
1.0.5 - 2020-12-29
|
1.0.5 - 2020-12-29
|
||||||
@ -133,7 +130,7 @@ Fixed
|
|||||||
|
|
||||||
Added
|
Added
|
||||||
~~~~~
|
~~~~~
|
||||||
* doas support as replacmeent for sudo on OpenBSD.
|
* doas support as replacement for sudo on OpenBSD.
|
||||||
* Added ChromeOS section to documentation (#262)
|
* Added ChromeOS section to documentation (#262)
|
||||||
* Add --no-sudo-pythonpath option
|
* Add --no-sudo-pythonpath option
|
||||||
|
|
||||||
|
13
README.rst
13
README.rst
@ -46,6 +46,14 @@ Obtaining sshuttle
|
|||||||
|
|
||||||
dnf install sshuttle
|
dnf install sshuttle
|
||||||
|
|
||||||
|
- openSUSE::
|
||||||
|
|
||||||
|
zypper in sshuttle
|
||||||
|
|
||||||
|
- Gentoo::
|
||||||
|
|
||||||
|
emerge -av net-proxy/sshuttle
|
||||||
|
|
||||||
- NixOS::
|
- NixOS::
|
||||||
|
|
||||||
nix-env -iA nixos.sshuttle
|
nix-env -iA nixos.sshuttle
|
||||||
@ -67,6 +75,11 @@ Obtaining sshuttle
|
|||||||
# pkg
|
# pkg
|
||||||
pkg install py36-sshuttle
|
pkg install py36-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.
|
It is also possible to install into a virtualenv as a non-root user.
|
||||||
|
|
||||||
- From PyPI::
|
- From PyPI::
|
||||||
|
@ -50,6 +50,14 @@ if [ "$FILE_NAME" == "" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Verify that the resulting file name begins with /etc/sudoers.d
|
||||||
|
FILE_NAME="$(realpath "/etc/sudoers.d/$FILE_NAME")"
|
||||||
|
if [[ "$FILE_NAME" != "/etc/sudoers.d/"* ]] ; then
|
||||||
|
echo -n "Invalid sudoers filename: Final sudoers file "
|
||||||
|
echo "location ($FILE_NAME) does not begin with /etc/sudoers.d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Make a temp file to hold the sudoers config
|
# Make a temp file to hold the sudoers config
|
||||||
umask 077
|
umask 077
|
||||||
TEMP_FILE=$(mktemp)
|
TEMP_FILE=$(mktemp)
|
||||||
@ -62,9 +70,9 @@ visudo_code=$?
|
|||||||
rm "$TEMP_FILE"
|
rm "$TEMP_FILE"
|
||||||
|
|
||||||
if [ $visudo_code -eq 0 ]; then
|
if [ $visudo_code -eq 0 ]; then
|
||||||
echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME"
|
echo "$CONTENT" > "$FILE_NAME"
|
||||||
chmod 0440 "/etc/sudoers.d/$FILE_NAME"
|
chmod 0440 "$FILE_NAME"
|
||||||
echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!"
|
echo "The sudoers file $FILE_NAME has been successfully created!"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
|
@ -9,4 +9,3 @@ stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and
|
|||||||
it just works, as do xterms and ssvncviewer etc.
|
it just works, as do xterms and ssvncviewer etc.
|
||||||
|
|
||||||
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide
|
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide
|
||||||
|
|
||||||
|
@ -34,4 +34,3 @@ sshuttle assembles the TCP stream locally, multiplexes it statefully over
|
|||||||
an ssh session, and disassembles it back into packets at the other end. So
|
an ssh session, and disassembles it back into packets at the other end. So
|
||||||
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
||||||
safe.
|
safe.
|
||||||
|
|
||||||
|
@ -26,4 +26,3 @@ Indices and tables
|
|||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ Installation
|
|||||||
|
|
||||||
pip install sshuttle
|
pip install sshuttle
|
||||||
|
|
||||||
- Debain package manager::
|
- Debian package manager::
|
||||||
|
|
||||||
sudo apt install sshuttle
|
sudo apt install sshuttle
|
||||||
|
|
||||||
|
179
docs/manpage.rst
179
docs/manpage.rst
@ -4,14 +4,14 @@ sshuttle
|
|||||||
|
|
||||||
Synopsis
|
Synopsis
|
||||||
--------
|
--------
|
||||||
**sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\>
|
**sshuttle** [*options*] **-r** *[username@]sshserver[:port]* \<*subnets* ...\>
|
||||||
|
|
||||||
|
|
||||||
Description
|
Description
|
||||||
-----------
|
-----------
|
||||||
:program:`sshuttle` allows you to create a VPN connection from your
|
:program:`sshuttle` allows you to create a VPN connection from your
|
||||||
machine to any remote server that you can connect to via
|
machine to any remote server that you can connect to via ssh, as long
|
||||||
ssh, as long as that server has python 3.6 or higher.
|
as that server has a sufficiently new Python installation.
|
||||||
|
|
||||||
To work, you must have root access on the local machine,
|
To work, you must have root access on the local machine,
|
||||||
but you can have a normal account on the server.
|
but you can have a normal account on the server.
|
||||||
@ -31,22 +31,23 @@ Options
|
|||||||
.. option:: <subnets>
|
.. option:: <subnets>
|
||||||
|
|
||||||
A list of subnets to route over the VPN, in the form
|
A list of subnets to route over the VPN, in the form
|
||||||
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
|
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
|
||||||
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
|
single IP address) and 1.2.3.4/32 (equivalent to 1.2.3.4),
|
||||||
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
|
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 netmask).
|
||||||
netmask), and 0/0 ('just route everything through the
|
Specify subnets 0/0 to match all IPv4 addresses and ::/0 to match
|
||||||
VPN'). Any of the previous examples are also valid if you append
|
all IPv6 addresses. Any of the previous examples are also valid if
|
||||||
a port or a port range, so 1.2.3.4:8000 will only tunnel traffic
|
you append a port or a port range, so 1.2.3.4:8000 will only
|
||||||
that has as the destination port 8000 of 1.2.3.4 and
|
tunnel traffic that has as the destination port 8000 of 1.2.3.4
|
||||||
1.2.3.0/24:8000-9000 will tunnel traffic going to any port between
|
and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port
|
||||||
8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet.
|
between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24
|
||||||
A hostname can be provided instead of an IP address. If the
|
subnet. A hostname can be provided instead of an IP address. If
|
||||||
hostname resolves to multiple IPs, all of the IPs are included.
|
the hostname resolves to multiple IPs, all of the IPs are
|
||||||
If a width is provided with a hostname that the width is applied
|
included. If a width is provided with a hostname, the width is
|
||||||
to all of the hostnames IPs (if they are all either IPv4 or IPv6).
|
applied to all of the hostnames IPs (if they are all either IPv4
|
||||||
Widths cannot be supplied to hostnames that resolve to both IPv4
|
or IPv6). Widths cannot be supplied to hostnames that resolve to
|
||||||
and IPv6. Valid examples are example.com, example.com:8000,
|
both IPv4 and IPv6. Valid examples are example.com,
|
||||||
example.com/24, example.com/24:8000 and example.com:8000-9000.
|
example.com:8000, example.com/24, example.com/24:8000 and
|
||||||
|
example.com:8000-9000.
|
||||||
|
|
||||||
.. option:: --method <auto|nat|nft|tproxy|pf|ipfw>
|
.. option:: --method <auto|nat|nft|tproxy|pf|ipfw>
|
||||||
|
|
||||||
@ -88,6 +89,13 @@ Options
|
|||||||
few subnets over the VPN, you probably would prefer to
|
few subnets over the VPN, you probably would prefer to
|
||||||
keep using your local DNS server for everything else.
|
keep using your local DNS server for everything else.
|
||||||
|
|
||||||
|
:program:`sshuttle` tries to store a cache of the hostnames in
|
||||||
|
~/.sshuttle.hosts on the remote host. Similarly, it tries to read
|
||||||
|
the file when you later reconnect to the host with --auto-hosts
|
||||||
|
enabled to quickly populate the host list. When troubleshooting
|
||||||
|
this feature, try removing this file on the remote host when
|
||||||
|
sshuttle is not running.
|
||||||
|
|
||||||
.. option:: -N, --auto-nets
|
.. option:: -N, --auto-nets
|
||||||
|
|
||||||
In addition to the subnets provided on the command
|
In addition to the subnets provided on the command
|
||||||
@ -141,7 +149,10 @@ Options
|
|||||||
The remote hostname and optional username and ssh
|
The remote hostname and optional username and ssh
|
||||||
port number to use for connecting to the remote server.
|
port number to use for connecting to the remote server.
|
||||||
For example, example.com, testuser@example.com,
|
For example, example.com, testuser@example.com,
|
||||||
testuser@example.com:2222, or example.com:2244.
|
testuser@example.com:2222, or example.com:2244. This
|
||||||
|
hostname is passed to ssh, so it will recognize any
|
||||||
|
aliases and settings you may have configured in
|
||||||
|
~/.ssh/config.
|
||||||
|
|
||||||
.. option:: -x <subnet>, --exclude=<subnet>
|
.. option:: -x <subnet>, --exclude=<subnet>
|
||||||
|
|
||||||
@ -174,7 +185,7 @@ Options
|
|||||||
|
|
||||||
A comma-separated list of hostnames to use to
|
A comma-separated list of hostnames to use to
|
||||||
initialize the :option:`--auto-hosts` scan algorithm.
|
initialize the :option:`--auto-hosts` scan algorithm.
|
||||||
:option:`--auto-hosts` does things like poll local SMB servers
|
:option:`--auto-hosts` does things like poll netstat output
|
||||||
for lists of local hostnames, but can speed things up
|
for lists of local hostnames, but can speed things up
|
||||||
if you use this option to give it a few names to start
|
if you use this option to give it a few names to start
|
||||||
from.
|
from.
|
||||||
@ -274,9 +285,10 @@ Options
|
|||||||
Set the file name for the sudoers.d file to be added. Default is
|
Set the file name for the sudoers.d file to be added. Default is
|
||||||
"sshuttle_auto". Only works with --sudoers.
|
"sshuttle_auto". Only works with --sudoers.
|
||||||
|
|
||||||
.. option:: -t, --tmark
|
.. option:: -t <mark>, --tmark=<mark>
|
||||||
|
|
||||||
Transproxy optional traffic mark with provided MARK value.
|
An option used by the tproxy method: Use the specified traffic
|
||||||
|
mark. The mark must be a hexadecimal value. Defaults to 0x01.
|
||||||
|
|
||||||
.. option:: --version
|
.. option:: --version
|
||||||
|
|
||||||
@ -305,54 +317,107 @@ Arguments read from a file must be one per line, as shown below::
|
|||||||
--option2
|
--option2
|
||||||
value2
|
value2
|
||||||
|
|
||||||
|
The configuration file supports comments for human-readable
|
||||||
|
annotations. For example::
|
||||||
|
|
||||||
|
# company-internal API
|
||||||
|
8.8.8.8/32
|
||||||
|
# home IoT
|
||||||
|
192.168.63.0/24
|
||||||
|
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
Test locally by proxying all local connections, without using ssh::
|
|
||||||
|
|
||||||
$ sshuttle -v 0/0
|
Use the following command to route all IPv4 TCP traffic through remote
|
||||||
|
(-r) host example.com (and possibly other traffic too, depending on
|
||||||
|
the selected --method). The 0/0 subnet, short for 0.0.0.0/0, matches
|
||||||
|
all IPv4 addresses. The ::/0 subnet, matching all IPv6 addresses could
|
||||||
|
be added to the example. We also exclude (-x) example.com:22 so that
|
||||||
|
we can establish ssh connections from our local machine to the remote
|
||||||
|
host without them being routed through sshuttle. Excluding the remote
|
||||||
|
host may be necessary on some machines for sshuttle to work properly.
|
||||||
|
Press Ctrl+C to exit. To also route DNS queries through sshuttle, try
|
||||||
|
adding --dns. Add or remove -v options to see more or less
|
||||||
|
information::
|
||||||
|
|
||||||
Starting sshuttle proxy.
|
$ sshuttle -r example.com -x example.com:22 0/0
|
||||||
Listening on ('0.0.0.0', 12300).
|
|
||||||
|
Starting sshuttle proxy (version ...).
|
||||||
[local sudo] Password:
|
[local sudo] Password:
|
||||||
firewall manager ready.
|
fw: Starting firewall with Python version 3.9.5
|
||||||
c : connecting to server...
|
fw: ready method name nat.
|
||||||
s: available routes:
|
c : IPv6 disabled since it isn't supported by method nat.
|
||||||
s: 192.168.42.0/24
|
c : Method: nat
|
||||||
c : connected.
|
c : IPv4: on
|
||||||
firewall manager: starting transproxy.
|
c : IPv6: off (not available with nat method)
|
||||||
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
|
c : UDP : off (not available with nat method)
|
||||||
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
|
c : DNS : off (available)
|
||||||
...etc...
|
c : User: off (available)
|
||||||
|
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
|
||||||
|
c : (<AddressFamily.AF_INET: 2>, '0.0.0.0', 0, 0, 0)
|
||||||
|
c : Subnets to exclude from forwarding:
|
||||||
|
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
|
||||||
|
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
|
||||||
|
c : TCP redirector listening on ('127.0.0.1', 12299).
|
||||||
|
c : Starting client with Python version 3.9.5
|
||||||
|
c : Connecting to server...
|
||||||
|
user@example.com's password:
|
||||||
|
s: Starting server with Python version 3.6.8
|
||||||
|
s: latency control setting = True
|
||||||
|
s: auto-nets:False
|
||||||
|
c : Connected to server.
|
||||||
|
fw: setting up.
|
||||||
|
fw: iptables -w -t nat -N sshuttle-12299
|
||||||
|
fw: iptables -w -t nat -F sshuttle-12299
|
||||||
|
...
|
||||||
|
Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
||||||
^C
|
^C
|
||||||
firewall manager: undoing changes.
|
|
||||||
KeyboardInterrupt
|
|
||||||
c : Keyboard interrupt: exiting.
|
c : Keyboard interrupt: exiting.
|
||||||
c : SW#8:192.168.42.121:47523: deleting
|
c : SW'unknown':Mux#1: deleting (1 remain)
|
||||||
c : SW#6:192.168.42.106:50035: deleting
|
c : SW#7:192.168.42.121:60554: deleting (0 remain)
|
||||||
|
|
||||||
Test connection to a remote server, with automatic hostname
|
|
||||||
|
Connect to a remote server, with automatic hostname
|
||||||
and subnet guessing::
|
and subnet guessing::
|
||||||
|
|
||||||
$ sshuttle -vNHr example.org
|
$ sshuttle -vNHr example.com -x example.com:22
|
||||||
|
Starting sshuttle proxy (version ...).
|
||||||
Starting sshuttle proxy.
|
[local sudo] Password:
|
||||||
Listening on ('0.0.0.0', 12300).
|
fw: Starting firewall with Python version 3.9.5
|
||||||
firewall manager ready.
|
fw: ready method name nat.
|
||||||
c : connecting to server...
|
c : IPv6 disabled since it isn't supported by method nat.
|
||||||
|
c : Method: nat
|
||||||
|
c : IPv4: on
|
||||||
|
c : IPv6: off (not available with nat method)
|
||||||
|
c : UDP : off (not available with nat method)
|
||||||
|
c : DNS : off (available)
|
||||||
|
c : User: off (available)
|
||||||
|
c : Subnets to forward through remote host (type, IP, cidr mask width, startPort, endPort):
|
||||||
|
c : NOTE: Additional subnets to forward may be added below by --auto-nets.
|
||||||
|
c : Subnets to exclude from forwarding:
|
||||||
|
c : (<AddressFamily.AF_INET: 2>, '...', 32, 22, 22)
|
||||||
|
c : (<AddressFamily.AF_INET: 2>, '127.0.0.1', 32, 0, 0)
|
||||||
|
c : TCP redirector listening on ('127.0.0.1', 12300).
|
||||||
|
c : Starting client with Python version 3.9.5
|
||||||
|
c : Connecting to server...
|
||||||
|
user@example.com's password:
|
||||||
|
s: Starting server with Python version 3.6.8
|
||||||
|
s: latency control setting = True
|
||||||
|
s: auto-nets:True
|
||||||
|
c : Connected to server.
|
||||||
|
c : seed_hosts: []
|
||||||
s: available routes:
|
s: available routes:
|
||||||
s: 77.141.99.0/24
|
s: 77.141.99.0/24
|
||||||
c : connected.
|
fw: setting up.
|
||||||
c : seed_hosts: []
|
fw: iptables -w -t nat -N sshuttle-12300
|
||||||
firewall manager: starting transproxy.
|
fw: iptables -w -t nat -F sshuttle-12300
|
||||||
hostwatch: Found: testbox1: 1.2.3.4
|
...
|
||||||
hostwatch: Found: mytest2: 5.6.7.8
|
|
||||||
hostwatch: Found: domaincontroller: 99.1.2.3
|
|
||||||
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
||||||
^C
|
^C
|
||||||
firewall manager: undoing changes.
|
|
||||||
c : Keyboard interrupt: exiting.
|
c : Keyboard interrupt: exiting.
|
||||||
c : SW#6:192.168.42.121:60554: deleting
|
c : SW'unknown':Mux#1: deleting (1 remain)
|
||||||
|
c : SW#7:192.168.42.121:60554: deleting (0 remain)
|
||||||
|
|
||||||
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
|
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
|
||||||
|
|
||||||
@ -376,9 +441,7 @@ Example configuration file::
|
|||||||
Discussion
|
Discussion
|
||||||
----------
|
----------
|
||||||
When it starts, :program:`sshuttle` creates an ssh session to the
|
When it starts, :program:`sshuttle` creates an ssh session to the
|
||||||
server specified by the ``-r`` option. If ``-r`` is omitted,
|
server specified by the ``-r`` option.
|
||||||
it will start both its client and server locally, which is
|
|
||||||
sometimes useful for testing.
|
|
||||||
|
|
||||||
After connecting to the remote server, :program:`sshuttle` uploads its
|
After connecting to the remote server, :program:`sshuttle` uploads its
|
||||||
(python) source code to the remote end and executes it
|
(python) source code to the remote end and executes it
|
||||||
|
@ -15,10 +15,12 @@ Supports:
|
|||||||
|
|
||||||
* IPv4 TCP
|
* IPv4 TCP
|
||||||
* IPv4 DNS
|
* IPv4 DNS
|
||||||
|
* IPv6 TCP
|
||||||
|
* IPv6 DNS
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* iptables DNAT, REDIRECT, and ttl modules.
|
* iptables DNAT and REDIRECT modules. ip6tables for IPv6.
|
||||||
|
|
||||||
Linux with nft method
|
Linux with nft method
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
@ -38,11 +40,11 @@ Linux with TPROXY method
|
|||||||
Supports:
|
Supports:
|
||||||
|
|
||||||
* IPv4 TCP
|
* IPv4 TCP
|
||||||
* IPv4 UDP (requires ``recvmsg`` - see below)
|
* IPv4 UDP
|
||||||
* IPv6 DNS (requires ``recvmsg`` - see below)
|
* IPv4 DNS
|
||||||
* IPv6 TCP
|
* IPv6 TCP
|
||||||
* IPv6 UDP (requires ``recvmsg`` - see below)
|
* IPv6 UDP
|
||||||
* IPv6 DNS (requires ``recvmsg`` - see below)
|
* IPv6 DNS
|
||||||
|
|
||||||
|
|
||||||
MacOS / FreeBSD / OpenBSD / pfSense
|
MacOS / FreeBSD / OpenBSD / pfSense
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
TPROXY
|
TPROXY
|
||||||
======
|
======
|
||||||
TPROXY is the only method that has full support of IPv6 and UDP.
|
TPROXY is the only method that supports UDP.
|
||||||
|
|
||||||
There are some things you need to consider for TPROXY to work:
|
There are some things you need to consider for TPROXY to work:
|
||||||
|
|
||||||
@ -12,24 +12,20 @@ There are some things you need to consider for TPROXY to work:
|
|||||||
ip -6 route add local default dev lo table 100
|
ip -6 route add local default dev lo table 100
|
||||||
ip -6 rule add fwmark {TMARK} lookup 100
|
ip -6 rule add fwmark {TMARK} lookup 100
|
||||||
|
|
||||||
where {TMARK} is the identifier mark passed with -t or --tmark flag (default value is 1).
|
where {TMARK} is the identifier mark passed with -t or --tmark flag
|
||||||
|
as a hexadecimal string (default value is '0x01').
|
||||||
|
|
||||||
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
|
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
|
||||||
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
|
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
|
||||||
|
|
||||||
- The client needs to be run as root. e.g.::
|
- The client needs to be run as root. e.g.::
|
||||||
|
|
||||||
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
||||||
|
|
||||||
- You may need to exclude the IP address of the server you are connecting to.
|
- You may need to exclude the IP address of the server you are connecting to.
|
||||||
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
|
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
|
||||||
work. Use the ``--exclude`` parameter for this.
|
work. Use the ``--exclude`` parameter for this.
|
||||||
|
|
||||||
- Similarly, UDP return packets (including DNS) could get intercepted and
|
|
||||||
bounced back. This is the case if you have a broad subnet such as
|
|
||||||
``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the
|
|
||||||
``--exclude`` parameter for this.
|
|
||||||
|
|
||||||
- You need the ``--method=tproxy`` parameter, as above.
|
- You need the ``--method=tproxy`` parameter, as above.
|
||||||
|
|
||||||
- The routes for the outgoing packets must already exist. For example, if your
|
- The routes for the outgoing packets must already exist. For example, if your
|
||||||
|
@ -33,4 +33,3 @@ That project I did for Slipstream was what first gave me the idea to merge
|
|||||||
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
||||||
program that was the best of all worlds. And here we are, at last.
|
program that was the best of all worlds. And here we are, at last.
|
||||||
You're welcome.
|
You're welcome.
|
||||||
|
|
||||||
|
@ -11,6 +11,10 @@ Forward all traffic::
|
|||||||
sshuttle -r username@sshserver 0.0.0.0/0
|
sshuttle -r username@sshserver 0.0.0.0/0
|
||||||
|
|
||||||
- Use the :option:`sshuttle -r` parameter to specify a remote server.
|
- Use the :option:`sshuttle -r` parameter to specify a remote server.
|
||||||
|
One some systems, you may also need to use the :option:`sshuttle -x`
|
||||||
|
parameter to exclude sshserver or sshserver:22 so that your local
|
||||||
|
machine can communicate directly to sshserver without it being
|
||||||
|
redirected by sshuttle.
|
||||||
|
|
||||||
- By default sshuttle will automatically choose a method to use. Override with
|
- By default sshuttle will automatically choose a method to use. Override with
|
||||||
the :option:`sshuttle --method` parameter.
|
the :option:`sshuttle --method` parameter.
|
||||||
@ -95,7 +99,7 @@ access to sshuttle. The default is `sshuttle_auto`::
|
|||||||
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
|
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
|
||||||
|
|
||||||
You can also see what configuration will be added to your system without
|
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
|
modifying anything. This can be helpful if the auto feature does not work, or
|
||||||
you want more control. This option also works with `--sudoers-username`.
|
you want more control. This option also works with `--sudoers-username`.
|
||||||
`--sudoers-filename` has no effect with this option::
|
`--sudoers-filename` has no effect with this option::
|
||||||
|
|
||||||
|
@ -16,4 +16,4 @@ Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can
|
|||||||
configure that in Vagrant), we can then ask Windows to route all its traffic
|
configure that in Vagrant), we can then ask Windows to route all its traffic
|
||||||
via the VM by running the following in cmd.exe with admin right::
|
via the VM by running the following in cmd.exe with admin right::
|
||||||
|
|
||||||
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200
|
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
attrs==20.3.0
|
pytest==6.2.5
|
||||||
pytest==6.2.1
|
pytest-cov==3.0.0
|
||||||
pytest-cov==2.10.1
|
flake8==4.0.1
|
||||||
mock==4.0.3
|
pyflakes==2.4.0
|
||||||
flake8==3.8.4
|
|
||||||
pyflakes==2.2.0
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
setuptools-scm==5.0.1
|
setuptools-scm==6.4.2
|
||||||
psutil
|
Sphinx==4.3.2
|
||||||
|
4
run
4
run
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
set -e
|
set -e
|
||||||
export PYTHONPATH="$(dirname $0):$PYTHONPATH"
|
export PYTHONPATH="$(dirname "$0"):$PYTHONPATH"
|
||||||
export PATH="$(dirname $0)/bin:$PATH"
|
export PATH="$(dirname "$0")/bin:$PATH"
|
||||||
|
|
||||||
python_best_version() {
|
python_best_version() {
|
||||||
if [ -x "$(command -v python3)" ] &&
|
if [ -x "$(command -v python3)" ] &&
|
||||||
|
2
setup.py
2
setup.py
@ -63,13 +63,11 @@ setup(
|
|||||||
},
|
},
|
||||||
python_requires='>=3.6',
|
python_requires='>=3.6',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'psutil',
|
|
||||||
],
|
],
|
||||||
tests_require=[
|
tests_require=[
|
||||||
'pytest',
|
'pytest',
|
||||||
'pytest-cov',
|
'pytest-cov',
|
||||||
'pytest-runner',
|
'pytest-runner',
|
||||||
'mock',
|
|
||||||
'flake8',
|
'flake8',
|
||||||
],
|
],
|
||||||
keywords="ssh vpn",
|
keywords="ssh vpn",
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
import types
|
import types
|
||||||
|
import platform
|
||||||
|
|
||||||
verbosity = verbosity # noqa: F821 must be a previously defined global
|
verbosity = verbosity # noqa: F821 must be a previously defined global
|
||||||
|
if verbosity > 0:
|
||||||
|
sys.stderr.write(' s: Running server on remote host with %s (version %s)\n'
|
||||||
|
% (sys.executable, platform.python_version()))
|
||||||
z = zlib.decompressobj()
|
z = zlib.decompressobj()
|
||||||
while 1:
|
while 1:
|
||||||
name = sys.stdin.readline().strip()
|
name = sys.stdin.readline().strip()
|
||||||
@ -14,7 +18,7 @@ while 1:
|
|||||||
name = name.decode("ASCII")
|
name = name.decode("ASCII")
|
||||||
nbytes = int(sys.stdin.readline())
|
nbytes = int(sys.stdin.readline())
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
sys.stderr.write(' s: assembling %r (%d bytes)\n'
|
sys.stderr.write(' s: assembling %r (%d bytes)\r\n'
|
||||||
% (name, nbytes))
|
% (name, nbytes))
|
||||||
content = z.decompress(sys.stdin.read(nbytes))
|
content = z.decompress(sys.stdin.read(nbytes))
|
||||||
|
|
||||||
@ -40,5 +44,6 @@ sshuttle.helpers.verbose = verbosity
|
|||||||
|
|
||||||
import sshuttle.cmdline_options as options # noqa: E402
|
import sshuttle.cmdline_options as options # noqa: E402
|
||||||
from sshuttle.server import main # noqa: E402
|
from sshuttle.server import main # noqa: E402
|
||||||
main(options.latency_control, options.auto_hosts, options.to_nameserver,
|
main(options.latency_control, options.latency_buffer_size,
|
||||||
|
options.auto_hosts, options.to_nameserver,
|
||||||
options.auto_nets)
|
options.auto_nets)
|
||||||
|
@ -6,7 +6,6 @@ import subprocess as ssubprocess
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
import psutil
|
|
||||||
|
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
import sshuttle.ssnet as ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
@ -23,28 +22,17 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
getpwnam = None
|
getpwnam = None
|
||||||
|
|
||||||
try:
|
import socket
|
||||||
# try getting recvmsg from python
|
|
||||||
import socket as pythonsocket
|
|
||||||
getattr(pythonsocket.socket, "recvmsg")
|
|
||||||
socket = pythonsocket
|
|
||||||
except AttributeError:
|
|
||||||
# try getting recvmsg from socket_ext library
|
|
||||||
try:
|
|
||||||
import socket_ext
|
|
||||||
getattr(socket_ext.socket, "recvmsg")
|
|
||||||
socket = socket_ext
|
|
||||||
except ImportError:
|
|
||||||
import socket
|
|
||||||
|
|
||||||
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
||||||
|
|
||||||
|
|
||||||
def got_signal(signum, frame):
|
def got_signal(signum, frame):
|
||||||
log('exiting on signal %d\n' % signum)
|
log('exiting on signal %d' % signum)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Filename of the pidfile created by the sshuttle client.
|
||||||
_pidname = None
|
_pidname = None
|
||||||
|
|
||||||
|
|
||||||
@ -57,7 +45,7 @@ def check_daemon(pidfile):
|
|||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
return # no pidfile, ok
|
return # no pidfile, ok
|
||||||
else:
|
else:
|
||||||
raise Fatal("c : can't read %s: %s" % (_pidname, e))
|
raise Fatal("can't read %s: %s" % (_pidname, e))
|
||||||
if not oldpid:
|
if not oldpid:
|
||||||
os.unlink(_pidname)
|
os.unlink(_pidname)
|
||||||
return # invalid pidfile, ok
|
return # invalid pidfile, ok
|
||||||
@ -80,13 +68,25 @@ def check_daemon(pidfile):
|
|||||||
|
|
||||||
|
|
||||||
def daemonize():
|
def daemonize():
|
||||||
|
# Try to open the pidfile prior to forking. If there is a problem,
|
||||||
|
# the client can then exit with a proper exit status code and
|
||||||
|
# message.
|
||||||
|
try:
|
||||||
|
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
|
||||||
|
except PermissionError:
|
||||||
|
# User will have to look in syslog for error message since
|
||||||
|
# --daemon implies --syslog, all output gets redirected to
|
||||||
|
# syslog.
|
||||||
|
raise Fatal("failed to create/write pidfile %s" % _pidname)
|
||||||
|
|
||||||
|
# Create a daemon process with a new session id.
|
||||||
if os.fork():
|
if os.fork():
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
os.setsid()
|
os.setsid()
|
||||||
if os.fork():
|
if os.fork():
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
|
# Write pid to the pidfile.
|
||||||
try:
|
try:
|
||||||
os.write(outfd, b'%d\n' % os.getpid())
|
os.write(outfd, b'%d\n' % os.getpid())
|
||||||
finally:
|
finally:
|
||||||
@ -152,7 +152,7 @@ class MultiListener:
|
|||||||
try:
|
try:
|
||||||
self.v4.listen(backlog)
|
self.v4.listen(backlog)
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
# on some systems v4 bind will fail if the v6 suceeded,
|
# on some systems v4 bind will fail if the v6 succeeded,
|
||||||
# in this case the v6 socket will receive v4 too.
|
# in this case the v6 socket will receive v4 too.
|
||||||
if e.errno == errno.EADDRINUSE and self.v6:
|
if e.errno == errno.EADDRINUSE and self.v6:
|
||||||
self.v4 = None
|
self.v4 = None
|
||||||
@ -177,19 +177,19 @@ class MultiListener:
|
|||||||
assert(self.bind_called)
|
assert(self.bind_called)
|
||||||
if self.v6:
|
if self.v6:
|
||||||
listenip = self.v6.getsockname()
|
listenip = self.v6.getsockname()
|
||||||
debug1('%s listening on %r.\n' % (what, listenip))
|
debug1('%s listening on %r.' % (what, listenip))
|
||||||
debug2('%s listening with %r.\n' % (what, self.v6))
|
debug2('%s listening with %r.' % (what, self.v6))
|
||||||
if self.v4:
|
if self.v4:
|
||||||
listenip = self.v4.getsockname()
|
listenip = self.v4.getsockname()
|
||||||
debug1('%s listening on %r.\n' % (what, listenip))
|
debug1('%s listening on %r.' % (what, listenip))
|
||||||
debug2('%s listening with %r.\n' % (what, self.v4))
|
debug2('%s listening with %r.' % (what, self.v4))
|
||||||
|
|
||||||
|
|
||||||
class FirewallClient:
|
class FirewallClient:
|
||||||
|
|
||||||
def __init__(self, method_name, sudo_pythonpath):
|
def __init__(self, method_name, sudo_pythonpath):
|
||||||
self.auto_nets = []
|
self.auto_nets = []
|
||||||
python_path = os.path.dirname(os.path.dirname(__file__))
|
|
||||||
argvbase = ([sys.executable, sys.argv[0]] +
|
argvbase = ([sys.executable, sys.argv[0]] +
|
||||||
['-v'] * (helpers.verbose or 0) +
|
['-v'] * (helpers.verbose or 0) +
|
||||||
['--method', method_name] +
|
['--method', method_name] +
|
||||||
@ -197,54 +197,101 @@ class FirewallClient:
|
|||||||
if ssyslog._p:
|
if ssyslog._p:
|
||||||
argvbase += ['--syslog']
|
argvbase += ['--syslog']
|
||||||
|
|
||||||
# Determine how to prefix the command in order to elevate privileges.
|
# A list of commands that we can try to run to start the firewall.
|
||||||
if platform.platform().startswith('OpenBSD'):
|
argv_tries = []
|
||||||
elev_prefix = ['doas'] # OpenBSD uses built in `doas`
|
|
||||||
|
if os.getuid() == 0: # No need to elevate privileges
|
||||||
|
argv_tries.append(argvbase)
|
||||||
else:
|
else:
|
||||||
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
|
# Linux typically uses sudo; OpenBSD uses doas. However, some
|
||||||
|
# Linux distributions are starting to use doas.
|
||||||
|
sudo_cmd = ['sudo', '-p', '[local sudo] Password: ']+argvbase
|
||||||
|
doas_cmd = ['doas']+argvbase
|
||||||
|
|
||||||
# Look for binary and switch to absolute path if we can find
|
# For clarity, try to replace executable name with the
|
||||||
# it.
|
# full path.
|
||||||
path = which(elev_prefix[0])
|
doas_path = which("doas")
|
||||||
if path:
|
if doas_path:
|
||||||
elev_prefix[0] = path
|
doas_cmd[0] = doas_path
|
||||||
|
sudo_path = which("sudo")
|
||||||
|
if sudo_path:
|
||||||
|
sudo_cmd[0] = sudo_path
|
||||||
|
|
||||||
if sudo_pythonpath:
|
# sudo_pythonpath indicates if we should set the
|
||||||
elev_prefix += ['/usr/bin/env',
|
# PYTHONPATH environment variable when elevating
|
||||||
'PYTHONPATH=%s' % python_path]
|
# privileges. This can be adjusted with the
|
||||||
argv_tries = [elev_prefix + argvbase, argvbase]
|
# --no-sudo-pythonpath option.
|
||||||
|
if sudo_pythonpath:
|
||||||
|
pp_prefix = ['/usr/bin/env',
|
||||||
|
'PYTHONPATH=%s' %
|
||||||
|
os.path.dirname(os.path.dirname(__file__))]
|
||||||
|
sudo_cmd = pp_prefix + sudo_cmd
|
||||||
|
doas_cmd = pp_prefix + doas_cmd
|
||||||
|
|
||||||
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
|
# If we can find doas and not sudo or if we are on
|
||||||
# because stupid Linux 'su' requires that stdin be attached to a tty.
|
# OpenBSD, try using doas first.
|
||||||
# Instead, attach a *bidirectional* socket to its stdout, and use
|
if (doas_path and not sudo_path) or \
|
||||||
# that for talking in both directions.
|
platform.platform().startswith('OpenBSD'):
|
||||||
(s1, s2) = socket.socketpair()
|
argv_tries = [doas_cmd, sudo_cmd, argvbase]
|
||||||
|
else:
|
||||||
|
argv_tries = [sudo_cmd, doas_cmd, argvbase]
|
||||||
|
|
||||||
def setup():
|
# Try all commands in argv_tries in order. If a command
|
||||||
# run in the child process
|
# produces an error, try the next one. If command is
|
||||||
s2.close()
|
# successful, set 'success' variable and break.
|
||||||
if os.getuid() == 0:
|
success = False
|
||||||
argv_tries = argv_tries[-1:] # last entry only
|
|
||||||
for argv in argv_tries:
|
for argv in argv_tries:
|
||||||
|
# we can't use stdin/stdout=subprocess.PIPE here, as we
|
||||||
|
# normally would, because stupid Linux 'su' requires that
|
||||||
|
# stdin be attached to a tty. Instead, attach a
|
||||||
|
# *bidirectional* socket to its stdout, and use that for
|
||||||
|
# talking in both directions.
|
||||||
|
(s1, s2) = socket.socketpair()
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
# run in the child process
|
||||||
|
s2.close()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if argv[0] == 'su':
|
debug1("Starting firewall manager with command: %r" % argv)
|
||||||
sys.stderr.write('[local su] ')
|
|
||||||
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
||||||
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
||||||
break
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log('Spawning firewall manager: %r\n' % argv)
|
# This exception will occur if the program isn't
|
||||||
raise Fatal(e)
|
# present or isn't executable.
|
||||||
self.argv = argv
|
debug1('Unable to start firewall manager. Popen failed. '
|
||||||
s1.close()
|
'Command=%r Exception=%s' % (argv, e))
|
||||||
self.pfile = s2.makefile('rwb')
|
continue
|
||||||
line = self.pfile.readline()
|
|
||||||
self.check()
|
self.argv = argv
|
||||||
if line[0:5] != b'READY':
|
s1.close()
|
||||||
raise Fatal('%r expected READY, got %r' % (self.argv, line))
|
self.pfile = s2.makefile('rwb')
|
||||||
method_name = line[6:-1]
|
line = self.pfile.readline()
|
||||||
self.method = get_method(method_name.decode("ASCII"))
|
|
||||||
self.method.set_firewall(self)
|
rv = self.p.poll() # Check if process is still running
|
||||||
|
if rv:
|
||||||
|
# We might get here if program runs and exits before
|
||||||
|
# outputting anything. For example, someone might have
|
||||||
|
# entered the wrong password to elevate privileges.
|
||||||
|
debug1('Unable to start firewall manager. '
|
||||||
|
'Process exited too early. '
|
||||||
|
'%r returned %d' % (self.argv, rv))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line[0:5] != b'READY':
|
||||||
|
debug1('Unable to start firewall manager. '
|
||||||
|
'Expected READY, got %r. '
|
||||||
|
'Command=%r' % (line, self.argv))
|
||||||
|
continue
|
||||||
|
|
||||||
|
method_name = line[6:-1]
|
||||||
|
self.method = get_method(method_name.decode("ASCII"))
|
||||||
|
self.method.set_firewall(self)
|
||||||
|
success = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise Fatal("All attempts to elevate privileges failed.")
|
||||||
|
|
||||||
def setup(self, subnets_include, subnets_exclude, nslist,
|
def setup(self, subnets_include, subnets_exclude, nslist,
|
||||||
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
|
||||||
@ -297,7 +344,8 @@ class FirewallClient:
|
|||||||
else:
|
else:
|
||||||
user = b'%d' % self.user
|
user = b'%d' % self.user
|
||||||
|
|
||||||
self.pfile.write(b'GO %d %s\n' % (udp, user))
|
self.pfile.write(b'GO %d %s %s %d\n' %
|
||||||
|
(udp, user, bytes(self.tmark, 'ascii'), os.getpid()))
|
||||||
self.pfile.flush()
|
self.pfile.flush()
|
||||||
|
|
||||||
line = self.pfile.readline()
|
line = self.pfile.readline()
|
||||||
@ -326,23 +374,23 @@ def expire_connections(now, mux):
|
|||||||
remove = []
|
remove = []
|
||||||
for chan, timeout in dnsreqs.items():
|
for chan, timeout in dnsreqs.items():
|
||||||
if timeout < now:
|
if timeout < now:
|
||||||
debug3('expiring dnsreqs channel=%d\n' % chan)
|
debug3('expiring dnsreqs channel=%d' % chan)
|
||||||
remove.append(chan)
|
remove.append(chan)
|
||||||
del mux.channels[chan]
|
del mux.channels[chan]
|
||||||
for chan in remove:
|
for chan in remove:
|
||||||
del dnsreqs[chan]
|
del dnsreqs[chan]
|
||||||
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
|
debug3('Remaining DNS requests: %d' % len(dnsreqs))
|
||||||
|
|
||||||
remove = []
|
remove = []
|
||||||
for peer, (chan, timeout) in udp_by_src.items():
|
for peer, (chan, timeout) in udp_by_src.items():
|
||||||
if timeout < now:
|
if timeout < now:
|
||||||
debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
|
debug3('expiring UDP channel channel=%d peer=%r' % (chan, peer))
|
||||||
mux.send(chan, ssnet.CMD_UDP_CLOSE, b'')
|
mux.send(chan, ssnet.CMD_UDP_CLOSE, b'')
|
||||||
remove.append(peer)
|
remove.append(peer)
|
||||||
del mux.channels[chan]
|
del mux.channels[chan]
|
||||||
for peer in remove:
|
for peer in remove:
|
||||||
del udp_by_src[peer]
|
del udp_by_src[peer]
|
||||||
debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
|
debug3('Remaining UDP channels: %d' % len(udp_by_src))
|
||||||
|
|
||||||
|
|
||||||
def onaccept_tcp(listener, method, mux, handlers):
|
def onaccept_tcp(listener, method, mux, handlers):
|
||||||
@ -351,7 +399,7 @@ def onaccept_tcp(listener, method, mux, handlers):
|
|||||||
sock, srcip = listener.accept()
|
sock, srcip = listener.accept()
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
|
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
|
||||||
debug1('Rejected incoming connection: too many open files!\n')
|
debug1('Rejected incoming connection: too many open files!')
|
||||||
# free up an fd so we can eat the connection
|
# free up an fd so we can eat the connection
|
||||||
os.close(_extra_fd)
|
os.close(_extra_fd)
|
||||||
try:
|
try:
|
||||||
@ -364,15 +412,15 @@ def onaccept_tcp(listener, method, mux, handlers):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
dstip = method.get_tcp_dstip(sock)
|
dstip = method.get_tcp_dstip(sock)
|
||||||
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
|
debug1('Accept TCP: %s:%r -> %s:%r.' % (srcip[0], srcip[1],
|
||||||
dstip[0], dstip[1]))
|
dstip[0], dstip[1]))
|
||||||
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
|
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
|
||||||
debug1("-- ignored: that's my address!\n")
|
debug1("-- ignored: that's my address!")
|
||||||
sock.close()
|
sock.close()
|
||||||
return
|
return
|
||||||
chan = mux.next_channel()
|
chan = mux.next_channel()
|
||||||
if not chan:
|
if not chan:
|
||||||
log('warning: too many open channels. Discarded connection.\n')
|
log('warning: too many open channels. Discarded connection.')
|
||||||
sock.close()
|
sock.close()
|
||||||
return
|
return
|
||||||
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
|
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
|
||||||
@ -385,7 +433,7 @@ def onaccept_tcp(listener, method, mux, handlers):
|
|||||||
def udp_done(chan, data, method, sock, dstip):
|
def udp_done(chan, data, method, sock, dstip):
|
||||||
(src, srcport, data) = data.split(b",", 2)
|
(src, srcport, data) = data.split(b",", 2)
|
||||||
srcip = (src, int(srcport))
|
srcip = (src, int(srcport))
|
||||||
debug3('doing send from %r to %r\n' % (srcip, dstip,))
|
debug3('doing send from %r to %r' % (srcip, dstip,))
|
||||||
method.send_udp(sock, srcip, dstip, data)
|
method.send_udp(sock, srcip, dstip, data)
|
||||||
|
|
||||||
|
|
||||||
@ -395,7 +443,7 @@ def onaccept_udp(listener, method, mux, handlers):
|
|||||||
if t is None:
|
if t is None:
|
||||||
return
|
return
|
||||||
srcip, dstip, data = t
|
srcip, dstip, data = t
|
||||||
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
|
debug1('Accept UDP: %r -> %r.' % (srcip, dstip,))
|
||||||
if srcip in udp_by_src:
|
if srcip in udp_by_src:
|
||||||
chan, _ = udp_by_src[srcip]
|
chan, _ = udp_by_src[srcip]
|
||||||
else:
|
else:
|
||||||
@ -412,7 +460,7 @@ def onaccept_udp(listener, method, mux, handlers):
|
|||||||
|
|
||||||
|
|
||||||
def dns_done(chan, data, method, sock, srcip, dstip, mux):
|
def dns_done(chan, data, method, sock, srcip, dstip, mux):
|
||||||
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
|
debug3('dns_done: channel=%d src=%r dst=%r' % (chan, srcip, dstip))
|
||||||
del mux.channels[chan]
|
del mux.channels[chan]
|
||||||
del dnsreqs[chan]
|
del dnsreqs[chan]
|
||||||
method.send_udp(sock, srcip, dstip, data)
|
method.send_udp(sock, srcip, dstip, data)
|
||||||
@ -427,9 +475,9 @@ def ondns(listener, method, mux, handlers):
|
|||||||
# dstip is None if we are using a method where we can't determine
|
# dstip is None if we are using a method where we can't determine
|
||||||
# the destination IP of the DNS request that we captured from the client.
|
# the destination IP of the DNS request that we captured from the client.
|
||||||
if dstip is None:
|
if dstip is None:
|
||||||
debug1('DNS request from %r: %d bytes\n' % (srcip, len(data)))
|
debug1('DNS request from %r: %d bytes' % (srcip, len(data)))
|
||||||
else:
|
else:
|
||||||
debug1('DNS request from %r to %r: %d bytes\n' %
|
debug1('DNS request from %r to %r: %d bytes' %
|
||||||
(srcip, dstip, len(data)))
|
(srcip, dstip, len(data)))
|
||||||
chan = mux.next_channel()
|
chan = mux.next_channel()
|
||||||
dnsreqs[chan] = now + 30
|
dnsreqs[chan] = now + 30
|
||||||
@ -440,30 +488,31 @@ def ondns(listener, method, mux, handlers):
|
|||||||
|
|
||||||
|
|
||||||
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control,
|
python, latency_control, latency_buffer_size,
|
||||||
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
|
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
|
||||||
to_nameserver):
|
to_nameserver):
|
||||||
|
|
||||||
helpers.logprefix = 'c : '
|
helpers.logprefix = 'c : '
|
||||||
debug1('Starting client with Python version %s\n'
|
debug1('Starting client with Python version %s'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
method = fw.method
|
method = fw.method
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
debug1('Connecting to server...\n')
|
debug1('Connecting to server...')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(serverproc, serversock) = ssh.connect(
|
(serverproc, serversock) = ssh.connect(
|
||||||
ssh_cmd, remotename, python,
|
ssh_cmd, remotename, python,
|
||||||
stderr=ssyslog._p and ssyslog._p.stdin,
|
stderr=ssyslog._p and ssyslog._p.stdin,
|
||||||
options=dict(latency_control=latency_control,
|
options=dict(latency_control=latency_control,
|
||||||
|
latency_buffer_size=latency_buffer_size,
|
||||||
auto_hosts=auto_hosts,
|
auto_hosts=auto_hosts,
|
||||||
to_nameserver=to_nameserver,
|
to_nameserver=to_nameserver,
|
||||||
auto_nets=auto_nets))
|
auto_nets=auto_nets))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.EPIPE:
|
if e.args[0] == errno.EPIPE:
|
||||||
raise Fatal("c : failed to establish ssh session (1)")
|
raise Fatal("failed to establish ssh session (1)")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
|
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
|
||||||
@ -481,22 +530,99 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
initstring = serversock.recv(len(expected))
|
initstring = serversock.recv(len(expected))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.ECONNRESET:
|
if e.args[0] == errno.ECONNRESET:
|
||||||
raise Fatal("c : failed to establish ssh session (2)")
|
raise Fatal("failed to establish ssh session (2)")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# Returns None if process is still running (or returns exit code)
|
||||||
rv = serverproc.poll()
|
rv = serverproc.poll()
|
||||||
if rv:
|
if rv is not None:
|
||||||
raise Fatal('c : server died with error code %d' % rv)
|
errmsg = "server died with error code %d\n" % rv
|
||||||
|
|
||||||
|
# Our fatal exceptions return exit code 99
|
||||||
|
if rv == 99:
|
||||||
|
errmsg += "This error code likely means that python started and " \
|
||||||
|
"the sshuttle server started. However, the sshuttle server " \
|
||||||
|
"may have raised a 'Fatal' exception after it started."
|
||||||
|
elif rv == 98:
|
||||||
|
errmsg += "This error code likely means that we were able to " \
|
||||||
|
"run python on the server, but that the program continued " \
|
||||||
|
"to the line after we call python's exec() to execute " \
|
||||||
|
"sshuttle's server code. Try specifying the python " \
|
||||||
|
"executable to user on the server by passing --python " \
|
||||||
|
"to sshuttle."
|
||||||
|
|
||||||
|
# This error should only be possible when --python is not specified.
|
||||||
|
elif rv == 97 and not python:
|
||||||
|
errmsg += "This error code likely means that either we " \
|
||||||
|
"couldn't find python3 or python in the PATH on the " \
|
||||||
|
"server or that we do not have permission to run 'exec' in " \
|
||||||
|
"the /bin/sh shell on the server. Try specifying the " \
|
||||||
|
"python executable to use on the server by passing " \
|
||||||
|
"--python to sshuttle."
|
||||||
|
|
||||||
|
# POSIX sh standards says error code 127 is used when you try
|
||||||
|
# to execute a program that does not exist. See section 2.8.2
|
||||||
|
# of
|
||||||
|
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08
|
||||||
|
elif rv == 127:
|
||||||
|
if python:
|
||||||
|
errmsg += "This error code likely means that we were not " \
|
||||||
|
"able to execute the python executable that specified " \
|
||||||
|
"with --python. You specified '%s'.\n" % python
|
||||||
|
if python.startswith("/"):
|
||||||
|
errmsg += "\nTip for users in a restricted shell on the " \
|
||||||
|
"server: The server may refuse to run programs " \
|
||||||
|
"specified with an absolute path. Try specifying " \
|
||||||
|
"just the name of the python executable. However, " \
|
||||||
|
"if python is not in your PATH and you cannot " \
|
||||||
|
"run programs specified with an absolute path, " \
|
||||||
|
"it is possible that sshuttle will not work."
|
||||||
|
else:
|
||||||
|
errmsg += "This error code likely means that we were unable " \
|
||||||
|
"to execute /bin/sh on the remote server. This can " \
|
||||||
|
"happen if /bin/sh does not exist on the server or if " \
|
||||||
|
"you are in a restricted shell that does not allow you " \
|
||||||
|
"to run programs specified with an absolute path. " \
|
||||||
|
"Try rerunning sshuttle with the --python parameter."
|
||||||
|
|
||||||
|
# When the redirected subnet includes the remote ssh host, the
|
||||||
|
# firewall rules can interrupt the ssh connection to the
|
||||||
|
# remote machine. This issue impacts some Linux machines. The
|
||||||
|
# user sees that the server dies with a broken pipe error and
|
||||||
|
# code 255.
|
||||||
|
#
|
||||||
|
# The solution to this problem is to exclude the remote
|
||||||
|
# server.
|
||||||
|
#
|
||||||
|
# There are many github issues from users encountering this
|
||||||
|
# problem. Most of the discussion on the topic is here:
|
||||||
|
# https://github.com/sshuttle/sshuttle/issues/191
|
||||||
|
elif rv == 255:
|
||||||
|
errmsg += "It might be possible to resolve this error by " \
|
||||||
|
"excluding the server that you are ssh'ing to. For example, " \
|
||||||
|
"if you are running 'sshuttle -v -r example.com 0/0' to " \
|
||||||
|
"redirect all traffic through example.com, then try " \
|
||||||
|
"'sshuttle -v -r example.com -x example.com 0/0' to " \
|
||||||
|
"exclude redirecting the connection to example.com itself " \
|
||||||
|
"(i.e., sshuttle's firewall rules may be breaking the " \
|
||||||
|
"ssh connection that it previously established). " \
|
||||||
|
"Alternatively, you may be able to use 'sshuttle -v -r " \
|
||||||
|
"example.com -x example.com:22 0/0' to redirect " \
|
||||||
|
"everything except ssh connections between your machine " \
|
||||||
|
"and example.com."
|
||||||
|
|
||||||
|
raise Fatal(errmsg)
|
||||||
|
|
||||||
if initstring != expected:
|
if initstring != expected:
|
||||||
raise Fatal('c : expected server init string %r; got %r'
|
raise Fatal('expected server init string %r; got %r'
|
||||||
% (expected, initstring))
|
% (expected, initstring))
|
||||||
log('Connected to server.\n')
|
log('Connected to server.')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
if daemon:
|
if daemon:
|
||||||
daemonize()
|
daemonize()
|
||||||
log('daemonizing (%s).\n' % _pidname)
|
log('daemonizing (%s).' % _pidname)
|
||||||
|
|
||||||
def onroutes(routestr):
|
def onroutes(routestr):
|
||||||
if auto_nets:
|
if auto_nets:
|
||||||
@ -508,11 +634,11 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
width = int(width)
|
width = int(width)
|
||||||
ip = ip.decode("ASCII")
|
ip = ip.decode("ASCII")
|
||||||
if family == socket.AF_INET6 and tcp_listener.v6 is None:
|
if family == socket.AF_INET6 and tcp_listener.v6 is None:
|
||||||
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
|
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
|
||||||
if family == socket.AF_INET and tcp_listener.v4 is None:
|
if family == socket.AF_INET and tcp_listener.v4 is None:
|
||||||
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
|
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
|
||||||
else:
|
else:
|
||||||
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width))
|
debug2("Adding auto net %d/%s/%d" % (family, ip, width))
|
||||||
fw.auto_nets.append((family, ip, width, 0, 0))
|
fw.auto_nets.append((family, ip, width, 0, 0))
|
||||||
|
|
||||||
# we definitely want to do this *after* starting ssh, or we might end
|
# we definitely want to do this *after* starting ssh, or we might end
|
||||||
@ -532,7 +658,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
|
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
|
||||||
|
|
||||||
def onhostlist(hostlist):
|
def onhostlist(hostlist):
|
||||||
debug2('got host list: %r\n' % hostlist)
|
debug2('got host list: %r' % hostlist)
|
||||||
for line in hostlist.strip().split():
|
for line in hostlist.strip().split():
|
||||||
if line:
|
if line:
|
||||||
name, ip = line.split(b',', 1)
|
name, ip = line.split(b',', 1)
|
||||||
@ -548,7 +674,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
dns_listener.add_handler(handlers, ondns, method, mux)
|
dns_listener.add_handler(handlers, ondns, method, mux)
|
||||||
|
|
||||||
if seed_hosts is not None:
|
if seed_hosts is not None:
|
||||||
debug1('seed_hosts: %r\n' % seed_hosts)
|
debug1('seed_hosts: %r' % seed_hosts)
|
||||||
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
|
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
|
||||||
|
|
||||||
def check_ssh_alive():
|
def check_ssh_alive():
|
||||||
@ -556,7 +682,9 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
# poll() won't tell us when process exited since the
|
# poll() won't tell us when process exited since the
|
||||||
# process is no longer our child (it returns 0 all the
|
# process is no longer our child (it returns 0 all the
|
||||||
# time).
|
# time).
|
||||||
if not psutil.pid_exists(serverproc.pid):
|
try:
|
||||||
|
os.kill(serverproc.pid, 0)
|
||||||
|
except OSError:
|
||||||
raise Fatal('ssh connection to server (pid %d) exited.' %
|
raise Fatal('ssh connection to server (pid %d) exited.' %
|
||||||
serverproc.pid)
|
serverproc.pid)
|
||||||
else:
|
else:
|
||||||
@ -574,40 +702,47 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
|
|
||||||
|
|
||||||
def main(listenip_v6, listenip_v4,
|
def main(listenip_v6, listenip_v4,
|
||||||
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
ssh_cmd, remotename, python, latency_control,
|
||||||
|
latency_buffer_size, dns, nslist,
|
||||||
method_name, seed_hosts, auto_hosts, auto_nets,
|
method_name, seed_hosts, auto_hosts, auto_nets,
|
||||||
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
||||||
user, sudo_pythonpath, tmark):
|
user, sudo_pythonpath, tmark):
|
||||||
|
|
||||||
if not remotename:
|
if not remotename:
|
||||||
print("WARNING: You must specify -r/--remote to securely route "
|
raise Fatal("You must use -r/--remote to specify a remote "
|
||||||
"traffic to a remote machine. Running without -r/--remote "
|
"host to route traffic through.")
|
||||||
"is only recommended for testing.")
|
|
||||||
|
|
||||||
if daemon:
|
if daemon:
|
||||||
try:
|
try:
|
||||||
check_daemon(pidfile)
|
check_daemon(pidfile)
|
||||||
except Fatal as e:
|
except Fatal as e:
|
||||||
log("%s\n" % e)
|
log("%s" % e)
|
||||||
return 5
|
return 5
|
||||||
debug1('Starting sshuttle proxy (version %s).\n' % __version__)
|
debug1('Starting sshuttle proxy (version %s).' % __version__)
|
||||||
helpers.logprefix = 'c : '
|
helpers.logprefix = 'c : '
|
||||||
|
|
||||||
fw = FirewallClient(method_name, sudo_pythonpath)
|
fw = FirewallClient(method_name, sudo_pythonpath)
|
||||||
|
|
||||||
# If --dns is used, store the IP addresses that the client
|
# nslist is the list of name severs to intercept. If --dns is
|
||||||
# normally uses for DNS lookups in nslist. The firewall needs to
|
# used, we add all DNS servers in resolv.conf. Otherwise, the list
|
||||||
# redirect packets outgoing to this server to the remote host
|
# can be populated with the --ns-hosts option (which is already
|
||||||
|
# stored in nslist). This list is used to setup the firewall so it
|
||||||
|
# can redirect packets outgoing to this server to the remote host
|
||||||
# instead.
|
# instead.
|
||||||
if dns:
|
if dns:
|
||||||
nslist += resolvconf_nameservers(True)
|
nslist += resolvconf_nameservers(True)
|
||||||
|
|
||||||
|
# If we are intercepting DNS requests, we tell the remote host
|
||||||
|
# where it should send the DNS requests to with the --to-ns
|
||||||
|
# option.
|
||||||
|
if len(nslist) > 0:
|
||||||
if to_nameserver is not None:
|
if to_nameserver is not None:
|
||||||
to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
|
to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
|
||||||
else:
|
else: # if we are not intercepting DNS traffic
|
||||||
# option doesn't make sense if we aren't proxying dns
|
# ...and the user specified a server to send DNS traffic to.
|
||||||
if to_nameserver and len(to_nameserver) > 0:
|
if to_nameserver and len(to_nameserver) > 0:
|
||||||
print("WARNING: --to-ns option is ignored because --dns was not "
|
print("WARNING: --to-ns option is ignored unless "
|
||||||
"used.")
|
"--dns or --ns-hosts is used.")
|
||||||
to_nameserver = None
|
to_nameserver = None
|
||||||
|
|
||||||
# Get family specific subnet lists. Also, the user may not specify
|
# Get family specific subnet lists. Also, the user may not specify
|
||||||
@ -643,14 +778,14 @@ def main(listenip_v6, listenip_v4,
|
|||||||
# "auto" when listen address is unspecified.
|
# "auto" when listen address is unspecified.
|
||||||
# The user specified address if provided by user
|
# The user specified address if provided by user
|
||||||
if listenip_v6 is None:
|
if listenip_v6 is None:
|
||||||
debug1("IPv6 disabled by --disable-ipv6\n")
|
debug1("IPv6 disabled by --disable-ipv6")
|
||||||
if listenip_v6 == "auto":
|
if listenip_v6 == "auto":
|
||||||
if avail.ipv6:
|
if avail.ipv6:
|
||||||
debug1("IPv6 enabled: Using default IPv6 listen address ::1\n")
|
debug1("IPv6 enabled: Using default IPv6 listen address ::1")
|
||||||
listenip_v6 = ('::1', 0)
|
listenip_v6 = ('::1', 0)
|
||||||
else:
|
else:
|
||||||
debug1("IPv6 disabled since it isn't supported by method "
|
debug1("IPv6 disabled since it isn't supported by method "
|
||||||
"%s.\n" % fw.method.name)
|
"%s." % fw.method.name)
|
||||||
listenip_v6 = None
|
listenip_v6 = None
|
||||||
|
|
||||||
# Make final decision about enabling IPv6:
|
# Make final decision about enabling IPv6:
|
||||||
@ -722,9 +857,9 @@ def main(listenip_v6, listenip_v4,
|
|||||||
msg += "(available)"
|
msg += "(available)"
|
||||||
else:
|
else:
|
||||||
msg += "(not available with %s method)" % fw.method.name
|
msg += "(not available with %s method)" % fw.method.name
|
||||||
debug1(msg + "\n")
|
debug1(msg)
|
||||||
|
|
||||||
debug1("Method: %s\n" % fw.method.name)
|
debug1("Method: %s" % fw.method.name)
|
||||||
feature_status("IPv4", required.ipv4, avail.ipv4)
|
feature_status("IPv4", required.ipv4, avail.ipv4)
|
||||||
feature_status("IPv6", required.ipv6, avail.ipv6)
|
feature_status("IPv6", required.ipv6, avail.ipv6)
|
||||||
feature_status("UDP ", required.udp, avail.udp)
|
feature_status("UDP ", required.udp, avail.udp)
|
||||||
@ -744,20 +879,20 @@ def main(listenip_v6, listenip_v4,
|
|||||||
# because we do that below when we have identified the ports to
|
# because we do that below when we have identified the ports to
|
||||||
# listen on.
|
# listen on.
|
||||||
debug1("Subnets to forward through remote host (type, IP, cidr mask "
|
debug1("Subnets to forward through remote host (type, IP, cidr mask "
|
||||||
"width, startPort, endPort):\n")
|
"width, startPort, endPort):")
|
||||||
for i in subnets_include:
|
for i in subnets_include:
|
||||||
debug1(" "+str(i)+"\n")
|
debug1(" "+str(i))
|
||||||
if auto_nets:
|
if auto_nets:
|
||||||
debug1("NOTE: Additional subnets to forward may be added below by "
|
debug1("NOTE: Additional subnets to forward may be added below by "
|
||||||
"--auto-nets.\n")
|
"--auto-nets.")
|
||||||
debug1("Subnets to exclude from forwarding:\n")
|
debug1("Subnets to exclude from forwarding:")
|
||||||
for i in subnets_exclude:
|
for i in subnets_exclude:
|
||||||
debug1(" "+str(i)+"\n")
|
debug1(" "+str(i))
|
||||||
if required.dns:
|
if required.dns:
|
||||||
debug1("DNS requests normally directed at these servers will be "
|
debug1("DNS requests normally directed at these servers will be "
|
||||||
"redirected to remote:\n")
|
"redirected to remote:")
|
||||||
for i in nslist:
|
for i in nslist:
|
||||||
debug1(" "+str(i)+"\n")
|
debug1(" "+str(i))
|
||||||
|
|
||||||
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
||||||
# if both ports given, no need to search for a spare port
|
# if both ports given, no need to search for a spare port
|
||||||
@ -775,7 +910,7 @@ def main(listenip_v6, listenip_v4,
|
|||||||
redirectport_v4 = 0
|
redirectport_v4 = 0
|
||||||
bound = False
|
bound = False
|
||||||
for port in ports:
|
for port in ports:
|
||||||
debug2('Trying to bind redirector on port %d\n' % port)
|
debug2('Trying to bind redirector on port %d' % port)
|
||||||
tcp_listener = MultiListener()
|
tcp_listener = MultiListener()
|
||||||
|
|
||||||
if required.udp:
|
if required.udp:
|
||||||
@ -830,7 +965,7 @@ def main(listenip_v6, listenip_v4,
|
|||||||
# search for spare port for DNS
|
# search for spare port for DNS
|
||||||
ports = range(12300, 9000, -1)
|
ports = range(12300, 9000, -1)
|
||||||
for port in ports:
|
for port in ports:
|
||||||
debug2('Trying to bind DNS redirector on port %d\n' % port)
|
debug2('Trying to bind DNS redirector on port %d' % port)
|
||||||
if port in used_ports:
|
if port in used_ports:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -908,8 +1043,9 @@ def main(listenip_v6, listenip_v4,
|
|||||||
# start the client process
|
# start the client process
|
||||||
try:
|
try:
|
||||||
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control, dns_listener,
|
python, latency_control, latency_buffer_size,
|
||||||
seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver)
|
dns_listener, seed_hosts, auto_hosts, auto_nets,
|
||||||
|
daemon, to_nameserver)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
if daemon:
|
if daemon:
|
||||||
|
@ -17,11 +17,11 @@ def main():
|
|||||||
if opt.sudoers or opt.sudoers_no_modify:
|
if opt.sudoers or opt.sudoers_no_modify:
|
||||||
if platform.platform().startswith('OpenBSD'):
|
if platform.platform().startswith('OpenBSD'):
|
||||||
log('Automatic sudoers does not work on BSD')
|
log('Automatic sudoers does not work on BSD')
|
||||||
exit(1)
|
return 1
|
||||||
|
|
||||||
if not opt.sudoers_filename:
|
if not opt.sudoers_filename:
|
||||||
log('--sudoers-file must be set or omited.')
|
log('--sudoers-file must be set or omitted.')
|
||||||
exit(1)
|
return 1
|
||||||
|
|
||||||
sudoers(
|
sudoers(
|
||||||
user_name=opt.sudoers_user,
|
user_name=opt.sudoers_user,
|
||||||
@ -85,6 +85,13 @@ def main():
|
|||||||
ipport_v4 = "auto"
|
ipport_v4 = "auto"
|
||||||
# parse_ipport6('[::1]:0')
|
# parse_ipport6('[::1]:0')
|
||||||
ipport_v6 = "auto" if not opt.disable_ipv6 else None
|
ipport_v6 = "auto" if not opt.disable_ipv6 else None
|
||||||
|
try:
|
||||||
|
int(opt.tmark, 16)
|
||||||
|
except ValueError:
|
||||||
|
parser.error("--tmark must be a hexadecimal value")
|
||||||
|
opt.tmark = opt.tmark.lower() # make 'x' in 0x lowercase
|
||||||
|
if not opt.tmark.startswith("0x"): # accept without 0x prefix
|
||||||
|
opt.tmark = "0x%s" % opt.tmark
|
||||||
if opt.syslog:
|
if opt.syslog:
|
||||||
ssyslog.start_syslog()
|
ssyslog.start_syslog()
|
||||||
ssyslog.close_stdin()
|
ssyslog.close_stdin()
|
||||||
@ -95,6 +102,7 @@ def main():
|
|||||||
remotename,
|
remotename,
|
||||||
opt.python,
|
opt.python,
|
||||||
opt.latency_control,
|
opt.latency_control,
|
||||||
|
opt.latency_buffer_size,
|
||||||
opt.dns,
|
opt.dns,
|
||||||
nslist,
|
nslist,
|
||||||
opt.method,
|
opt.method,
|
||||||
@ -117,9 +125,9 @@ def main():
|
|||||||
return return_code
|
return return_code
|
||||||
|
|
||||||
except Fatal as e:
|
except Fatal as e:
|
||||||
log('fatal: %s\n' % e)
|
log('fatal: %s' % e)
|
||||||
return 99
|
return 99
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log('\n')
|
log('\n')
|
||||||
log('Keyboard interrupt: exiting.\n')
|
log('Keyboard interrupt: exiting.')
|
||||||
return 1
|
return 1
|
||||||
|
@ -5,13 +5,15 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import traceback
|
import traceback
|
||||||
|
import subprocess as ssubprocess
|
||||||
|
|
||||||
import sshuttle.ssyslog as ssyslog
|
import sshuttle.ssyslog as ssyslog
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
from sshuttle.helpers import debug1, debug2, Fatal
|
from sshuttle.helpers import log, debug1, debug2, Fatal
|
||||||
from sshuttle.methods import get_auto_method, get_method
|
from sshuttle.methods import get_auto_method, get_method
|
||||||
|
|
||||||
HOSTSFILE = '/etc/hosts'
|
HOSTSFILE = '/etc/hosts'
|
||||||
|
sshuttle_pid = None
|
||||||
|
|
||||||
|
|
||||||
def rewrite_etc_hosts(hostmap, port):
|
def rewrite_etc_hosts(hostmap, port):
|
||||||
@ -51,26 +53,51 @@ def rewrite_etc_hosts(hostmap, port):
|
|||||||
def restore_etc_hosts(hostmap, port):
|
def restore_etc_hosts(hostmap, port):
|
||||||
# Only restore if we added hosts to /etc/hosts previously.
|
# Only restore if we added hosts to /etc/hosts previously.
|
||||||
if len(hostmap) > 0:
|
if len(hostmap) > 0:
|
||||||
debug2('undoing /etc/hosts changes.\n')
|
debug2('undoing /etc/hosts changes.')
|
||||||
rewrite_etc_hosts({}, port)
|
rewrite_etc_hosts({}, port)
|
||||||
|
|
||||||
|
|
||||||
|
def firewall_exit(signum, frame):
|
||||||
|
# The typical sshuttle exit is that the main sshuttle process
|
||||||
|
# exits, closes file descriptors it uses, and the firewall process
|
||||||
|
# notices that it can't read from stdin anymore and exits
|
||||||
|
# (cleaning up firewall rules).
|
||||||
|
#
|
||||||
|
# However, in some cases, Ctrl+C might get sent to the firewall
|
||||||
|
# process. This might caused if someone manually tries to kill the
|
||||||
|
# firewall process, or if sshuttle was started using sudo's use_pty option
|
||||||
|
# and they try to exit by pressing Ctrl+C. Here, we forward the
|
||||||
|
# Ctrl+C/SIGINT to the main sshuttle process which should trigger
|
||||||
|
# the typical exit process as described above.
|
||||||
|
global sshuttle_pid
|
||||||
|
if sshuttle_pid:
|
||||||
|
debug1("Relaying SIGINT to sshuttle process %d\n" % sshuttle_pid)
|
||||||
|
os.kill(sshuttle_pid, signal.SIGINT)
|
||||||
|
|
||||||
|
|
||||||
# Isolate function that needs to be replaced for tests
|
# Isolate function that needs to be replaced for tests
|
||||||
def setup_daemon():
|
def setup_daemon():
|
||||||
if os.getuid() != 0:
|
if os.getuid() != 0:
|
||||||
raise Fatal('fw: '
|
raise Fatal('You must be root (or enable su/sudo) to set the firewall')
|
||||||
'You must be root (or enable su/sudo) to set the firewall')
|
|
||||||
|
|
||||||
# don't disappear if our controlling terminal or stdout/stderr
|
# don't disappear if our controlling terminal or stdout/stderr
|
||||||
# disappears; we still have to clean up.
|
# disappears; we still have to clean up.
|
||||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
signal.signal(signal.SIGTERM, firewall_exit)
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
signal.signal(signal.SIGINT, firewall_exit)
|
||||||
|
|
||||||
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
# Calling setsid() here isn't strictly necessary. However, it forces
|
||||||
# I'll die automatically.
|
# Ctrl+C to get sent to the main sshuttle process instead of to
|
||||||
os.setsid()
|
# the firewall process---which is our preferred way to shutdown.
|
||||||
|
# Nonetheless, if the firewall process receives a SIGTERM/SIGINT
|
||||||
|
# signal, it will relay a SIGINT to the main sshuttle process
|
||||||
|
# automatically.
|
||||||
|
try:
|
||||||
|
os.setsid()
|
||||||
|
except OSError:
|
||||||
|
# setsid() fails if sudo is configured with the use_pty option.
|
||||||
|
pass
|
||||||
|
|
||||||
# because of limitations of the 'su' command, the *real* stdin/stdout
|
# because of limitations of the 'su' command, the *real* stdin/stdout
|
||||||
# are both attached to stdout initially. Clone stdout into stdin so we
|
# are both attached to stdout initially. Clone stdout into stdin so we
|
||||||
@ -90,19 +117,50 @@ def subnet_weight(s):
|
|||||||
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
|
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
|
||||||
|
|
||||||
|
|
||||||
|
def flush_systemd_dns_cache():
|
||||||
|
# If the user is using systemd-resolve for DNS resolution, it is
|
||||||
|
# possible for the request to go through systemd-resolve before we
|
||||||
|
# see it...and it may use a cached result instead of sending a
|
||||||
|
# request that we can intercept. When sshuttle starts and stops,
|
||||||
|
# this means that we should clear the cache!
|
||||||
|
#
|
||||||
|
# The command to do this was named systemd-resolve, but changed to
|
||||||
|
# resolvectl in systemd 239.
|
||||||
|
# https://github.com/systemd/systemd/blob/f8eb41003df1a4eab59ff9bec67b2787c9368dbd/NEWS#L3816
|
||||||
|
|
||||||
|
p = None
|
||||||
|
if helpers.which("resolvectl"):
|
||||||
|
debug2("Flushing systemd's DNS resolver cache: "
|
||||||
|
"resolvectl flush-caches")
|
||||||
|
p = ssubprocess.Popen(["resolvectl", "flush-caches"],
|
||||||
|
stdout=ssubprocess.PIPE, env=helpers.get_env())
|
||||||
|
elif helpers.which("systemd-resolve"):
|
||||||
|
debug2("Flushing systemd's DNS resolver cache: "
|
||||||
|
"systemd-resolve --flush-caches")
|
||||||
|
p = ssubprocess.Popen(["systemd-resolve", "--flush-caches"],
|
||||||
|
stdout=ssubprocess.PIPE, env=helpers.get_env())
|
||||||
|
|
||||||
|
if p:
|
||||||
|
# Wait so flush is finished and process doesn't show up as defunct.
|
||||||
|
rv = p.wait()
|
||||||
|
if rv != 0:
|
||||||
|
log("Received non-zero return code %d when flushing DNS resolver "
|
||||||
|
"cache." % rv)
|
||||||
|
|
||||||
|
|
||||||
# This is some voodoo for setting up the kernel's transparent
|
# This is some voodoo for setting up the kernel's transparent
|
||||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||||
# otherwise we delete it, then make them from scratch.
|
# otherwise we delete it, then make them from scratch.
|
||||||
#
|
#
|
||||||
# This code is supposed to clean up after itself by deleting its rules on
|
# This code is supposed to clean up after itself by deleting its rules on
|
||||||
# exit. In case that fails, it's not the end of the world; future runs will
|
# exit. In case that fails, it's not the end of the world; future runs will
|
||||||
# supercede it in the transproxy list, at least, so the leftover rules
|
# supersede it in the transproxy list, at least, so the leftover rules
|
||||||
# are hopefully harmless.
|
# are hopefully harmless.
|
||||||
def main(method_name, syslog):
|
def main(method_name, syslog):
|
||||||
|
helpers.logprefix = 'fw: '
|
||||||
stdin, stdout = setup_daemon()
|
stdin, stdout = setup_daemon()
|
||||||
hostmap = {}
|
hostmap = {}
|
||||||
helpers.logprefix = 'fw: '
|
debug1('Starting firewall with Python version %s'
|
||||||
debug1('Starting firewall with Python version %s\n'
|
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
if method_name == "auto":
|
if method_name == "auto":
|
||||||
@ -119,7 +177,7 @@ def main(method_name, syslog):
|
|||||||
"Check that the appropriate programs are in your "
|
"Check that the appropriate programs are in your "
|
||||||
"PATH." % method_name)
|
"PATH." % method_name)
|
||||||
|
|
||||||
debug1('ready method name %s.\n' % method.name)
|
debug1('ready method name %s.' % method.name)
|
||||||
stdout.write('READY %s\n' % method.name)
|
stdout.write('READY %s\n' % method.name)
|
||||||
stdout.flush()
|
stdout.flush()
|
||||||
|
|
||||||
@ -136,14 +194,14 @@ def main(method_name, syslog):
|
|||||||
while 1:
|
while 1:
|
||||||
line = stdin.readline(128)
|
line = stdin.readline(128)
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('fw: expected route but got %r' % line)
|
raise Fatal('expected route but got %r' % line)
|
||||||
elif line.startswith("NSLIST\n"):
|
elif line.startswith("NSLIST\n"):
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, width, exclude, ip, fport, lport) = \
|
(family, width, exclude, ip, fport, lport) = \
|
||||||
line.strip().split(',', 5)
|
line.strip().split(',', 5)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
raise Fatal('fw: expected route or NSLIST but got %r' % line)
|
raise Fatal('expected route or NSLIST but got %r' % line)
|
||||||
subnets.append((
|
subnets.append((
|
||||||
int(family),
|
int(family),
|
||||||
int(width),
|
int(width),
|
||||||
@ -151,31 +209,31 @@ def main(method_name, syslog):
|
|||||||
ip,
|
ip,
|
||||||
int(fport),
|
int(fport),
|
||||||
int(lport)))
|
int(lport)))
|
||||||
debug2('Got subnets: %r\n' % subnets)
|
debug2('Got subnets: %r' % subnets)
|
||||||
|
|
||||||
nslist = []
|
nslist = []
|
||||||
if line != 'NSLIST\n':
|
if line != 'NSLIST\n':
|
||||||
raise Fatal('fw: expected NSLIST but got %r' % line)
|
raise Fatal('expected NSLIST but got %r' % line)
|
||||||
while 1:
|
while 1:
|
||||||
line = stdin.readline(128)
|
line = stdin.readline(128)
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('fw: expected nslist but got %r' % line)
|
raise Fatal('expected nslist but got %r' % line)
|
||||||
elif line.startswith("PORTS "):
|
elif line.startswith("PORTS "):
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, ip) = line.strip().split(',', 1)
|
(family, ip) = line.strip().split(',', 1)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
raise Fatal('fw: expected nslist or PORTS but got %r' % line)
|
raise Fatal('expected nslist or PORTS but got %r' % line)
|
||||||
nslist.append((int(family), ip))
|
nslist.append((int(family), ip))
|
||||||
debug2('Got partial nslist: %r\n' % nslist)
|
debug2('Got partial nslist: %r' % nslist)
|
||||||
debug2('Got nslist: %r\n' % nslist)
|
debug2('Got nslist: %r' % nslist)
|
||||||
|
|
||||||
if not line.startswith('PORTS '):
|
if not line.startswith('PORTS '):
|
||||||
raise Fatal('fw: expected PORTS but got %r' % line)
|
raise Fatal('expected PORTS but got %r' % line)
|
||||||
_, _, ports = line.partition(" ")
|
_, _, ports = line.partition(" ")
|
||||||
ports = ports.split(",")
|
ports = ports.split(",")
|
||||||
if len(ports) != 4:
|
if len(ports) != 4:
|
||||||
raise Fatal('fw: expected 4 ports but got %d' % len(ports))
|
raise Fatal('expected 4 ports but got %d' % len(ports))
|
||||||
port_v6 = int(ports[0])
|
port_v6 = int(ports[0])
|
||||||
port_v4 = int(ports[1])
|
port_v4 = int(ports[1])
|
||||||
dnsport_v6 = int(ports[2])
|
dnsport_v6 = int(ports[2])
|
||||||
@ -190,21 +248,24 @@ def main(method_name, syslog):
|
|||||||
assert(dnsport_v4 >= 0)
|
assert(dnsport_v4 >= 0)
|
||||||
assert(dnsport_v4 <= 65535)
|
assert(dnsport_v4 <= 65535)
|
||||||
|
|
||||||
debug2('Got ports: %d,%d,%d,%d\n'
|
debug2('Got ports: %d,%d,%d,%d'
|
||||||
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
||||||
|
|
||||||
line = stdin.readline(128)
|
line = stdin.readline(128)
|
||||||
if not line:
|
if not line:
|
||||||
raise Fatal('fw: expected GO but got %r' % line)
|
raise Fatal('expected GO but got %r' % line)
|
||||||
elif not line.startswith("GO "):
|
elif not line.startswith("GO "):
|
||||||
raise Fatal('fw: expected GO but got %r' % line)
|
raise Fatal('expected GO but got %r' % line)
|
||||||
|
|
||||||
_, _, args = line.partition(" ")
|
_, _, args = line.partition(" ")
|
||||||
udp, user = args.strip().split(" ", 1)
|
global sshuttle_pid
|
||||||
|
udp, user, tmark, sshuttle_pid = args.strip().split(" ", 3)
|
||||||
udp = bool(int(udp))
|
udp = bool(int(udp))
|
||||||
|
sshuttle_pid = int(sshuttle_pid)
|
||||||
if user == '-':
|
if user == '-':
|
||||||
user = None
|
user = None
|
||||||
debug2('Got udp: %r, user: %r\n' % (udp, user))
|
debug2('Got udp: %r, user: %r, tmark: %s, sshuttle_pid: %d' %
|
||||||
|
(udp, user, tmark, sshuttle_pid))
|
||||||
|
|
||||||
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
||||||
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
||||||
@ -212,22 +273,23 @@ def main(method_name, syslog):
|
|||||||
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
debug1('setting up.\n')
|
debug1('setting up.')
|
||||||
|
|
||||||
if subnets_v6 or nslist_v6:
|
if subnets_v6 or nslist_v6:
|
||||||
debug2('setting up IPv6.\n')
|
debug2('setting up IPv6.')
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
port_v6, dnsport_v6, nslist_v6,
|
port_v6, dnsport_v6, nslist_v6,
|
||||||
socket.AF_INET6, subnets_v6, udp,
|
socket.AF_INET6, subnets_v6, udp,
|
||||||
user)
|
user, tmark)
|
||||||
|
|
||||||
if subnets_v4 or nslist_v4:
|
if subnets_v4 or nslist_v4:
|
||||||
debug2('setting up IPv4.\n')
|
debug2('setting up IPv4.')
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
port_v4, dnsport_v4, nslist_v4,
|
port_v4, dnsport_v4, nslist_v4,
|
||||||
socket.AF_INET, subnets_v4, udp,
|
socket.AF_INET, subnets_v4, udp,
|
||||||
user)
|
user, tmark)
|
||||||
|
|
||||||
|
flush_systemd_dns_cache()
|
||||||
stdout.write('STARTED\n')
|
stdout.write('STARTED\n')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -245,40 +307,38 @@ def main(method_name, syslog):
|
|||||||
if line.startswith('HOST '):
|
if line.startswith('HOST '):
|
||||||
(name, ip) = line[5:].strip().split(',', 1)
|
(name, ip) = line[5:].strip().split(',', 1)
|
||||||
hostmap[name] = ip
|
hostmap[name] = ip
|
||||||
debug2('setting up /etc/hosts.\n')
|
debug2('setting up /etc/hosts.')
|
||||||
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
|
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
|
||||||
elif line:
|
elif line:
|
||||||
if not method.firewall_command(line):
|
if not method.firewall_command(line):
|
||||||
raise Fatal('fw: expected command, got %r' % line)
|
raise Fatal('expected command, got %r' % line)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
debug1('undoing changes.\n')
|
debug1('undoing changes.')
|
||||||
except BaseException:
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if subnets_v6 or nslist_v6:
|
if subnets_v6 or nslist_v6:
|
||||||
debug2('undoing IPv6 changes.\n')
|
debug2('undoing IPv6 changes.')
|
||||||
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
|
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("Error trying to undo IPv6 firewall.\n")
|
debug1("Error trying to undo IPv6 firewall.")
|
||||||
for line in traceback.format_exc().splitlines():
|
debug1(traceback.format_exc())
|
||||||
debug1("---> %s\n" % line)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if subnets_v4 or nslist_v4:
|
if subnets_v4 or nslist_v4:
|
||||||
debug2('undoing IPv4 changes.\n')
|
debug2('undoing IPv4 changes.')
|
||||||
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
|
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("Error trying to undo IPv4 firewall.\n")
|
debug1("Error trying to undo IPv4 firewall.")
|
||||||
for line in traceback.format_exc().splitlines():
|
debug1(traceback.format_exc())
|
||||||
debug1("---> %s\n" % line)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
@ -287,8 +347,16 @@ def main(method_name, syslog):
|
|||||||
restore_etc_hosts(hostmap, port_v6 or port_v4)
|
restore_etc_hosts(hostmap, port_v6 or port_v4)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("Error trying to undo /etc/hosts changes.\n")
|
debug1("Error trying to undo /etc/hosts changes.")
|
||||||
for line in traceback.format_exc().splitlines():
|
debug1(traceback.format_exc())
|
||||||
debug1("---> %s\n" % line)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
debug2('An error occurred, ignoring it.')
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
flush_systemd_dns_cache()
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
debug1("Error trying to flush systemd dns cache.")
|
||||||
|
debug1(traceback.format_exc())
|
||||||
|
except BaseException:
|
||||||
|
debug2("An error occurred, ignoring it.")
|
||||||
|
@ -15,14 +15,22 @@ def log(s):
|
|||||||
global logprefix
|
global logprefix
|
||||||
try:
|
try:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
if s.find("\n") != -1:
|
# Put newline at end of string if line doesn't have one.
|
||||||
prefix = logprefix
|
if not s.endswith("\n"):
|
||||||
s = s.rstrip("\n")
|
s = s+"\n"
|
||||||
for line in s.split("\n"):
|
|
||||||
sys.stderr.write(prefix + line + "\n")
|
prefix = logprefix
|
||||||
prefix = "---> "
|
s = s.rstrip("\n")
|
||||||
else:
|
for line in s.split("\n"):
|
||||||
sys.stderr.write(logprefix + s)
|
# We output with \r\n instead of \n because when we use
|
||||||
|
# sudo with the use_pty option, the firewall process, the
|
||||||
|
# other processes printing to the terminal will have the
|
||||||
|
# \n move to the next line, but they will fail to reset
|
||||||
|
# cursor to the beginning of the line. Printing output
|
||||||
|
# with \r\n endings fixes that problem and does not appear
|
||||||
|
# to cause problems elsewhere.
|
||||||
|
sys.stderr.write(prefix + line + "\r\n")
|
||||||
|
prefix = " "
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
except IOError:
|
except IOError:
|
||||||
# this could happen if stderr gets forcibly disconnected, eg. because
|
# this could happen if stderr gets forcibly disconnected, eg. because
|
||||||
@ -91,11 +99,11 @@ def resolvconf_nameservers(systemd_resolved):
|
|||||||
words = line.lower().split()
|
words = line.lower().split()
|
||||||
if len(words) >= 2 and words[0] == 'nameserver':
|
if len(words) >= 2 and words[0] == 'nameserver':
|
||||||
this_file_nsservers.append(family_ip_tuple(words[1]))
|
this_file_nsservers.append(family_ip_tuple(words[1]))
|
||||||
debug2("Found DNS servers in %s: %s\n" %
|
debug2("Found DNS servers in %s: %s" %
|
||||||
(f, [n[1] for n in this_file_nsservers]))
|
(f, [n[1] for n in this_file_nsservers]))
|
||||||
nsservers += this_file_nsservers
|
nsservers += this_file_nsservers
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
debug3("Failed to read %s when looking for DNS servers: %s\n" %
|
debug3("Failed to read %s when looking for DNS servers: %s" %
|
||||||
(f, e.strerror))
|
(f, e.strerror))
|
||||||
|
|
||||||
return nsservers
|
return nsservers
|
||||||
@ -215,7 +223,7 @@ def which(file, mode=os.F_OK | os.X_OK):
|
|||||||
path = get_path()
|
path = get_path()
|
||||||
rv = _which(file, mode, path)
|
rv = _which(file, mode, path)
|
||||||
if rv:
|
if rv:
|
||||||
debug2("which() found '%s' at %s\n" % (file, rv))
|
debug2("which() found '%s' at %s" % (file, rv))
|
||||||
else:
|
else:
|
||||||
debug2("which() could not find '%s' in %s\n" % (file, path))
|
debug2("which() could not find '%s' in %s" % (file, path))
|
||||||
return rv
|
return rv
|
||||||
|
@ -15,16 +15,16 @@ POLL_TIME = 60 * 15
|
|||||||
NETSTAT_POLL_TIME = 30
|
NETSTAT_POLL_TIME = 30
|
||||||
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
|
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
|
||||||
|
|
||||||
|
# Have we already failed to write CACHEFILE?
|
||||||
|
CACHE_WRITE_FAILED = False
|
||||||
|
|
||||||
_nmb_ok = True
|
|
||||||
_smb_ok = True
|
|
||||||
hostnames = {}
|
hostnames = {}
|
||||||
queue = {}
|
queue = {}
|
||||||
try:
|
try:
|
||||||
null = open(os.devnull, 'wb')
|
null = open(os.devnull, 'wb')
|
||||||
except IOError:
|
except IOError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log('warning: %s\n' % e)
|
log('warning: %s' % e)
|
||||||
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +33,10 @@ def _is_ip(s):
|
|||||||
|
|
||||||
|
|
||||||
def write_host_cache():
|
def write_host_cache():
|
||||||
|
"""If possible, write our hosts file to disk so future connections
|
||||||
|
can reuse the hosts that we already found."""
|
||||||
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
|
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
|
||||||
|
global CACHE_WRITE_FAILED
|
||||||
try:
|
try:
|
||||||
f = open(tmpname, 'wb')
|
f = open(tmpname, 'wb')
|
||||||
for name, ip in sorted(hostnames.items()):
|
for name, ip in sorted(hostnames.items()):
|
||||||
@ -41,7 +44,15 @@ def write_host_cache():
|
|||||||
f.close()
|
f.close()
|
||||||
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
|
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
|
||||||
os.rename(tmpname, CACHEFILE)
|
os.rename(tmpname, CACHEFILE)
|
||||||
finally:
|
CACHE_WRITE_FAILED = False
|
||||||
|
except (OSError, IOError):
|
||||||
|
# Write message if we haven't yet or if we get a failure after
|
||||||
|
# a previous success.
|
||||||
|
if not CACHE_WRITE_FAILED:
|
||||||
|
log("Failed to write host cache to temporary file "
|
||||||
|
"%s and rename it to %s" % (tmpname, CACHEFILE))
|
||||||
|
CACHE_WRITE_FAILED = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.unlink(tmpname)
|
os.unlink(tmpname)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@ -49,25 +60,34 @@ def write_host_cache():
|
|||||||
|
|
||||||
|
|
||||||
def read_host_cache():
|
def read_host_cache():
|
||||||
|
"""If possible, read the cache file from disk to populate hosts that
|
||||||
|
were found in a previous sshuttle run."""
|
||||||
try:
|
try:
|
||||||
f = open(CACHEFILE)
|
f = open(CACHEFILE)
|
||||||
except IOError:
|
except (OSError, IOError):
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
raise
|
log("Failed to read existing host cache file %s on remote host"
|
||||||
|
% CACHEFILE)
|
||||||
|
return
|
||||||
for line in f:
|
for line in f:
|
||||||
words = line.strip().split(',')
|
words = line.strip().split(',')
|
||||||
if len(words) == 2:
|
if len(words) == 2:
|
||||||
(name, ip) = words
|
(name, ip) = words
|
||||||
name = re.sub(r'[^-\w\.]', '-', name).strip()
|
name = re.sub(r'[^-\w\.]', '-', name).strip()
|
||||||
|
# Remove characters that shouldn't be in IP
|
||||||
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
||||||
if name and ip:
|
if name and ip:
|
||||||
found_host(name, ip)
|
found_host(name, ip)
|
||||||
|
|
||||||
|
|
||||||
def found_host(name, ip):
|
def found_host(name, ip):
|
||||||
|
"""The provided name maps to the given IP. Add the host to the
|
||||||
|
hostnames list, send the host to the sshuttle client via
|
||||||
|
stdout, and write the host to the cache file.
|
||||||
|
"""
|
||||||
hostname = re.sub(r'\..*', '', name)
|
hostname = re.sub(r'\..*', '', name)
|
||||||
hostname = re.sub(r'[^-\w\.]', '_', hostname)
|
hostname = re.sub(r'[^-\w\.]', '_', hostname)
|
||||||
if (ip.startswith('127.') or ip.startswith('255.') or
|
if (ip.startswith('127.') or ip.startswith('255.') or
|
||||||
@ -80,43 +100,51 @@ def found_host(name, ip):
|
|||||||
oldip = hostnames.get(name)
|
oldip = hostnames.get(name)
|
||||||
if oldip != ip:
|
if oldip != ip:
|
||||||
hostnames[name] = ip
|
hostnames[name] = ip
|
||||||
debug1('Found: %s: %s\n' % (name, ip))
|
debug1('Found: %s: %s' % (name, ip))
|
||||||
sys.stdout.write('%s,%s\n' % (name, ip))
|
sys.stdout.write('%s,%s\n' % (name, ip))
|
||||||
write_host_cache()
|
write_host_cache()
|
||||||
|
|
||||||
|
|
||||||
def _check_etc_hosts():
|
def _check_etc_hosts():
|
||||||
debug2(' > hosts\n')
|
"""If possible, read /etc/hosts to find hosts."""
|
||||||
for line in open('/etc/hosts'):
|
filename = '/etc/hosts'
|
||||||
line = re.sub(r'#.*', '', line)
|
debug2(' > Reading %s on remote host' % filename)
|
||||||
words = line.strip().split()
|
try:
|
||||||
if not words:
|
for line in open(filename):
|
||||||
continue
|
line = re.sub(r'#.*', '', line) # remove comments
|
||||||
ip = words[0]
|
words = line.strip().split()
|
||||||
names = words[1:]
|
if not words:
|
||||||
if _is_ip(ip):
|
continue
|
||||||
debug3('< %s %r\n' % (ip, names))
|
ip = words[0]
|
||||||
for n in names:
|
if _is_ip(ip):
|
||||||
check_host(n)
|
names = words[1:]
|
||||||
found_host(n, ip)
|
debug3('< %s %r' % (ip, names))
|
||||||
|
for n in names:
|
||||||
|
check_host(n)
|
||||||
|
found_host(n, ip)
|
||||||
|
except (OSError, IOError):
|
||||||
|
debug1("Failed to read %s on remote host" % filename)
|
||||||
|
|
||||||
|
|
||||||
def _check_revdns(ip):
|
def _check_revdns(ip):
|
||||||
debug2(' > rev: %s\n' % ip)
|
"""Use reverse DNS to try to get hostnames from an IP addresses."""
|
||||||
|
debug2(' > rev: %s' % ip)
|
||||||
try:
|
try:
|
||||||
r = socket.gethostbyaddr(ip)
|
r = socket.gethostbyaddr(ip)
|
||||||
debug3('< %s\n' % r[0])
|
debug3('< %s' % r[0])
|
||||||
check_host(r[0])
|
check_host(r[0])
|
||||||
found_host(r[0], ip)
|
found_host(r[0], ip)
|
||||||
except (socket.herror, UnicodeError):
|
except (OSError, socket.error, UnicodeError):
|
||||||
|
# This case is expected to occur regularly.
|
||||||
|
# debug3('< %s gethostbyaddr failed on remote host' % ip)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _check_dns(hostname):
|
def _check_dns(hostname):
|
||||||
debug2(' > dns: %s\n' % hostname)
|
debug2(' > dns: %s' % hostname)
|
||||||
try:
|
try:
|
||||||
ip = socket.gethostbyname(hostname)
|
ip = socket.gethostbyname(hostname)
|
||||||
debug3('< %s\n' % ip)
|
debug3('< %s' % ip)
|
||||||
check_host(ip)
|
check_host(ip)
|
||||||
found_host(hostname, ip)
|
found_host(hostname, ip)
|
||||||
except (socket.gaierror, UnicodeError):
|
except (socket.gaierror, UnicodeError):
|
||||||
@ -124,7 +152,7 @@ def _check_dns(hostname):
|
|||||||
|
|
||||||
|
|
||||||
def _check_netstat():
|
def _check_netstat():
|
||||||
debug2(' > netstat\n')
|
debug2(' > netstat')
|
||||||
argv = ['netstat', '-n']
|
argv = ['netstat', '-n']
|
||||||
try:
|
try:
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||||
@ -133,118 +161,26 @@ def _check_netstat():
|
|||||||
p.wait()
|
p.wait()
|
||||||
except OSError:
|
except OSError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r' % (argv, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# The same IPs may appear multiple times. Consolidate them so the
|
||||||
|
# debug message doesn't print the same IP repeatedly.
|
||||||
|
ip_list = []
|
||||||
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
|
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
|
||||||
debug3('< %s\n' % ip)
|
if ip not in ip_list:
|
||||||
|
ip_list.append(ip)
|
||||||
|
|
||||||
|
for ip in sorted(ip_list):
|
||||||
|
debug3('< %s' % ip)
|
||||||
check_host(ip)
|
check_host(ip)
|
||||||
|
|
||||||
|
|
||||||
def _check_smb(hostname):
|
|
||||||
return
|
|
||||||
global _smb_ok
|
|
||||||
if not _smb_ok:
|
|
||||||
return
|
|
||||||
debug2(' > smb: %s\n' % hostname)
|
|
||||||
argv = ['smbclient', '-U', '%', '-L', hostname]
|
|
||||||
try:
|
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
|
||||||
env=get_env())
|
|
||||||
lines = p.stdout.readlines()
|
|
||||||
p.wait()
|
|
||||||
except OSError:
|
|
||||||
_, e = sys.exc_info()[:2]
|
|
||||||
log('%r failed: %r\n' % (argv, e))
|
|
||||||
_smb_ok = False
|
|
||||||
return
|
|
||||||
|
|
||||||
lines.reverse()
|
|
||||||
|
|
||||||
# junk at top
|
|
||||||
while lines:
|
|
||||||
line = lines.pop().strip()
|
|
||||||
if re.match(r'Server\s+', line):
|
|
||||||
break
|
|
||||||
|
|
||||||
# server list section:
|
|
||||||
# Server Comment
|
|
||||||
# ------ -------
|
|
||||||
while lines:
|
|
||||||
line = lines.pop().strip()
|
|
||||||
if not line or re.match(r'-+\s+-+', line):
|
|
||||||
continue
|
|
||||||
if re.match(r'Workgroup\s+Master', line):
|
|
||||||
break
|
|
||||||
words = line.split()
|
|
||||||
hostname = words[0].lower()
|
|
||||||
debug3('< %s\n' % hostname)
|
|
||||||
check_host(hostname)
|
|
||||||
|
|
||||||
# workgroup list section:
|
|
||||||
# Workgroup Master
|
|
||||||
# --------- ------
|
|
||||||
while lines:
|
|
||||||
line = lines.pop().strip()
|
|
||||||
if re.match(r'-+\s+', line):
|
|
||||||
continue
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
words = line.split()
|
|
||||||
(workgroup, hostname) = (words[0].lower(), words[1].lower())
|
|
||||||
debug3('< group(%s) -> %s\n' % (workgroup, hostname))
|
|
||||||
check_host(hostname)
|
|
||||||
check_workgroup(workgroup)
|
|
||||||
|
|
||||||
if lines:
|
|
||||||
assert(0)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_nmb(hostname, is_workgroup, is_master):
|
|
||||||
return
|
|
||||||
global _nmb_ok
|
|
||||||
if not _nmb_ok:
|
|
||||||
return
|
|
||||||
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
|
|
||||||
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
|
|
||||||
try:
|
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
|
||||||
env=get_env)
|
|
||||||
lines = p.stdout.readlines()
|
|
||||||
rv = p.wait()
|
|
||||||
except OSError:
|
|
||||||
_, e = sys.exc_info()[:2]
|
|
||||||
log('%r failed: %r\n' % (argv, e))
|
|
||||||
_nmb_ok = False
|
|
||||||
return
|
|
||||||
if rv:
|
|
||||||
log('%r returned %d\n' % (argv, rv))
|
|
||||||
return
|
|
||||||
for line in lines:
|
|
||||||
m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line)
|
|
||||||
if m:
|
|
||||||
g = m.groups()
|
|
||||||
(ip, name) = (g[0], g[1].lower())
|
|
||||||
debug3('< %s -> %s\n' % (name, ip))
|
|
||||||
if is_workgroup:
|
|
||||||
_enqueue(_check_smb, ip)
|
|
||||||
else:
|
|
||||||
found_host(name, ip)
|
|
||||||
check_host(name)
|
|
||||||
|
|
||||||
|
|
||||||
def check_host(hostname):
|
def check_host(hostname):
|
||||||
if _is_ip(hostname):
|
if _is_ip(hostname):
|
||||||
_enqueue(_check_revdns, hostname)
|
_enqueue(_check_revdns, hostname)
|
||||||
else:
|
else:
|
||||||
_enqueue(_check_dns, hostname)
|
_enqueue(_check_dns, hostname)
|
||||||
_enqueue(_check_smb, hostname)
|
|
||||||
_enqueue(_check_nmb, hostname, False, False)
|
|
||||||
|
|
||||||
|
|
||||||
def check_workgroup(hostname):
|
|
||||||
_enqueue(_check_nmb, hostname, True, False)
|
|
||||||
_enqueue(_check_nmb, hostname, True, True)
|
|
||||||
|
|
||||||
|
|
||||||
def _enqueue(op, *args):
|
def _enqueue(op, *args):
|
||||||
@ -263,12 +199,9 @@ def _stdin_still_ok(timeout):
|
|||||||
|
|
||||||
|
|
||||||
def hw_main(seed_hosts, auto_hosts):
|
def hw_main(seed_hosts, auto_hosts):
|
||||||
if helpers.verbose >= 2:
|
helpers.logprefix = 'HH: '
|
||||||
helpers.logprefix = 'HH: '
|
|
||||||
else:
|
|
||||||
helpers.logprefix = 'hostwatch: '
|
|
||||||
|
|
||||||
debug1('Starting hostwatch with Python version %s\n'
|
debug1('Starting hostwatch with Python version %s'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
for h in seed_hosts:
|
for h in seed_hosts:
|
||||||
@ -280,18 +213,22 @@ def hw_main(seed_hosts, auto_hosts):
|
|||||||
_enqueue(_check_netstat)
|
_enqueue(_check_netstat)
|
||||||
check_host('localhost')
|
check_host('localhost')
|
||||||
check_host(socket.gethostname())
|
check_host(socket.gethostname())
|
||||||
check_workgroup('workgroup')
|
|
||||||
check_workgroup('-')
|
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
# For each item in the queue
|
||||||
for t, last_polled in list(queue.items()):
|
for t, last_polled in list(queue.items()):
|
||||||
(op, args) = t
|
(op, args) = t
|
||||||
if not _stdin_still_ok(0):
|
if not _stdin_still_ok(0):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Determine if we need to run.
|
||||||
maxtime = POLL_TIME
|
maxtime = POLL_TIME
|
||||||
|
# netstat runs more often than other jobs
|
||||||
if op == _check_netstat:
|
if op == _check_netstat:
|
||||||
maxtime = NETSTAT_POLL_TIME
|
maxtime = NETSTAT_POLL_TIME
|
||||||
|
|
||||||
|
# Check if this jobs needs to run.
|
||||||
if now - last_polled > maxtime:
|
if now - last_polled > maxtime:
|
||||||
queue[t] = time.time()
|
queue[t] = time.time()
|
||||||
op(*args)
|
op(*args)
|
||||||
@ -301,5 +238,5 @@ def hw_main(seed_hosts, auto_hosts):
|
|||||||
break
|
break
|
||||||
|
|
||||||
# FIXME: use a smarter timeout based on oldest last_polled
|
# FIXME: use a smarter timeout based on oldest last_polled
|
||||||
if not _stdin_still_ok(1):
|
if not _stdin_still_ok(1): # sleeps for up to 1 second
|
||||||
break
|
break
|
||||||
|
@ -7,7 +7,7 @@ def nonfatal(func, *args):
|
|||||||
try:
|
try:
|
||||||
func(*args)
|
func(*args)
|
||||||
except Fatal as e:
|
except Fatal as e:
|
||||||
log('fw: error: %s\n' % e)
|
log('error: %s' % e)
|
||||||
|
|
||||||
|
|
||||||
def ipt_chain_exists(family, table, name):
|
def ipt_chain_exists(family, table, name):
|
||||||
@ -17,27 +17,27 @@ def ipt_chain_exists(family, table, name):
|
|||||||
cmd = 'iptables'
|
cmd = 'iptables'
|
||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
argv = [cmd, '-t', table, '-nL']
|
argv = [cmd, '-w', '-t', table, '-nL']
|
||||||
try:
|
try:
|
||||||
output = ssubprocess.check_output(argv, env=get_env())
|
output = ssubprocess.check_output(argv, env=get_env())
|
||||||
for line in output.decode('ASCII').split('\n'):
|
for line in output.decode('ASCII').split('\n'):
|
||||||
if line.startswith('Chain %s ' % name):
|
if line.startswith('Chain %s ' % name):
|
||||||
return True
|
return True
|
||||||
except ssubprocess.CalledProcessError as e:
|
except ssubprocess.CalledProcessError as e:
|
||||||
raise Fatal('fw: %r returned %d' % (argv, e.returncode))
|
raise Fatal('%r returned %d' % (argv, e.returncode))
|
||||||
|
|
||||||
|
|
||||||
def ipt(family, table, *args):
|
def ipt(family, table, *args):
|
||||||
if family == socket.AF_INET6:
|
if family == socket.AF_INET6:
|
||||||
argv = ['ip6tables', '-t', table] + list(args)
|
argv = ['ip6tables', '-w', '-t', table] + list(args)
|
||||||
elif family == socket.AF_INET:
|
elif family == socket.AF_INET:
|
||||||
argv = ['iptables', '-t', table] + list(args)
|
argv = ['iptables', '-w', '-t', table] + list(args)
|
||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
debug1('%s\n' % ' '.join(argv))
|
debug1('%s' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv, env=get_env())
|
rv = ssubprocess.call(argv, env=get_env())
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('fw: %r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
def nft(family, table, action, *args):
|
def nft(family, table, action, *args):
|
||||||
@ -45,29 +45,7 @@ def nft(family, table, action, *args):
|
|||||||
argv = ['nft', action, 'inet', table] + list(args)
|
argv = ['nft', action, 'inet', table] + list(args)
|
||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
debug1('%s\n' % ' '.join(argv))
|
debug1('%s' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv, env=get_env())
|
rv = ssubprocess.call(argv, env=get_env())
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('fw: %r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
_no_ttl_module = False
|
|
||||||
|
|
||||||
|
|
||||||
def ipt_ttl(family, *args):
|
|
||||||
global _no_ttl_module
|
|
||||||
if not _no_ttl_module:
|
|
||||||
# we avoid infinite loops by generating server-side connections
|
|
||||||
# with ttl 63. This makes the client side not recapture those
|
|
||||||
# connections, in case client == server.
|
|
||||||
try:
|
|
||||||
argsplus = list(args)
|
|
||||||
ipt(family, *argsplus)
|
|
||||||
except Fatal:
|
|
||||||
ipt(family, *args)
|
|
||||||
# we only get here if the non-ttl attempt succeeds
|
|
||||||
log('fw: WARNING: your iptables is missing '
|
|
||||||
'the ttl module.\n')
|
|
||||||
_no_ttl_module = True
|
|
||||||
else:
|
|
||||||
ipt(family, *args)
|
|
||||||
|
@ -66,7 +66,7 @@ class BaseMethod(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def recv_udp(udp_listener, bufsize):
|
def recv_udp(udp_listener, bufsize):
|
||||||
debug3('Accept UDP using recvfrom.\n')
|
debug3('Accept UDP using recvfrom.')
|
||||||
data, srcip = udp_listener.recvfrom(bufsize)
|
data, srcip = udp_listener.recvfrom(bufsize)
|
||||||
return (srcip, None, data)
|
return (srcip, None, data)
|
||||||
|
|
||||||
@ -87,11 +87,11 @@ class BaseMethod(object):
|
|||||||
for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
|
for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
|
||||||
if getattr(features, key) and not getattr(avail, key):
|
if getattr(features, key) and not getattr(avail, key):
|
||||||
raise Fatal(
|
raise Fatal(
|
||||||
"Feature %s not supported with method %s.\n" %
|
"Feature %s not supported with method %s." %
|
||||||
(key, self.name))
|
(key, self.name))
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user):
|
user, tmark):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
@ -108,13 +108,13 @@ def get_method(method_name):
|
|||||||
|
|
||||||
|
|
||||||
def get_auto_method():
|
def get_auto_method():
|
||||||
debug3("Selecting a method automatically...\n")
|
debug3("Selecting a method automatically...")
|
||||||
# Try these methods, in order:
|
# Try these methods, in order:
|
||||||
methods_to_try = ["nat", "nft", "pf", "ipfw"]
|
methods_to_try = ["nat", "nft", "pf", "ipfw"]
|
||||||
for m in methods_to_try:
|
for m in methods_to_try:
|
||||||
method = get_method(m)
|
method = get_method(m)
|
||||||
if method.is_supported():
|
if method.is_supported():
|
||||||
debug3("Method '%s' was automatically selected.\n" % m)
|
debug3("Method '%s' was automatically selected." % m)
|
||||||
return method
|
return method
|
||||||
|
|
||||||
raise Fatal("Unable to automatically find a supported method. Check that "
|
raise Fatal("Unable to automatically find a supported method. Check that "
|
||||||
|
@ -4,72 +4,40 @@ from sshuttle.methods import BaseMethod
|
|||||||
from sshuttle.helpers import log, debug1, debug2, debug3, \
|
from sshuttle.helpers import log, debug1, debug2, debug3, \
|
||||||
Fatal, family_to_string, get_env, which
|
Fatal, family_to_string, get_env, which
|
||||||
|
|
||||||
recvmsg = None
|
import socket
|
||||||
try:
|
|
||||||
# try getting recvmsg from python
|
|
||||||
import socket as pythonsocket
|
|
||||||
getattr(pythonsocket.socket, "recvmsg")
|
|
||||||
socket = pythonsocket
|
|
||||||
recvmsg = "python"
|
|
||||||
except AttributeError:
|
|
||||||
# try getting recvmsg from socket_ext library
|
|
||||||
try:
|
|
||||||
import socket_ext
|
|
||||||
getattr(socket_ext.socket, "recvmsg")
|
|
||||||
socket = socket_ext
|
|
||||||
recvmsg = "socket_ext"
|
|
||||||
except ImportError:
|
|
||||||
import socket
|
|
||||||
|
|
||||||
IP_BINDANY = 24
|
IP_BINDANY = 24
|
||||||
IP_RECVDSTADDR = 7
|
IP_RECVDSTADDR = 7
|
||||||
SOL_IPV6 = 41
|
SOL_IPV6 = 41
|
||||||
IPV6_RECVDSTADDR = 74
|
IPV6_RECVDSTADDR = 74
|
||||||
|
|
||||||
if recvmsg == "python":
|
|
||||||
def recv_udp(listener, bufsize):
|
def recv_udp(listener, bufsize):
|
||||||
debug3('Accept UDP python using recvmsg.\n')
|
debug3('Accept UDP python using recvmsg.')
|
||||||
data, ancdata, _, srcip = listener.recvmsg(4096,
|
data, ancdata, _, srcip = listener.recvmsg(4096,
|
||||||
socket.CMSG_SPACE(4))
|
socket.CMSG_SPACE(4))
|
||||||
dstip = None
|
dstip = None
|
||||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
||||||
port = 53
|
port = 53
|
||||||
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
|
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
|
||||||
dstip = (ip, port)
|
dstip = (ip, port)
|
||||||
break
|
break
|
||||||
return (srcip, dstip, data)
|
return (srcip, dstip, data)
|
||||||
elif recvmsg == "socket_ext":
|
|
||||||
def recv_udp(listener, bufsize):
|
|
||||||
debug3('Accept UDP using socket_ext recvmsg.\n')
|
|
||||||
srcip, data, adata, _ = listener.recvmsg((bufsize,),
|
|
||||||
socket.CMSG_SPACE(4))
|
|
||||||
dstip = None
|
|
||||||
for a in adata:
|
|
||||||
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR:
|
|
||||||
port = 53
|
|
||||||
ip = socket.inet_ntop(socket.AF_INET, a.cmsg_data[0:4])
|
|
||||||
dstip = (ip, port)
|
|
||||||
break
|
|
||||||
return (srcip, dstip, data[0])
|
|
||||||
else:
|
|
||||||
def recv_udp(listener, bufsize):
|
|
||||||
debug3('Accept UDP using recvfrom.\n')
|
|
||||||
data, srcip = listener.recvfrom(bufsize)
|
|
||||||
return (srcip, None, data)
|
|
||||||
|
|
||||||
|
|
||||||
def ipfw_rule_exists(n):
|
def ipfw_rule_exists(n):
|
||||||
argv = ['ipfw', 'list']
|
argv = ['ipfw', 'list', '%d' % n]
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
||||||
|
|
||||||
found = False
|
found = False
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
if line.startswith(b'%05d ' % n):
|
if line.startswith(b'%05d ' % n):
|
||||||
if not ('ipttl 63' in line or 'check-state' in line):
|
if 'check-state :sshuttle' not in line:
|
||||||
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
log('non-sshuttle ipfw rule: %r' % line.strip())
|
||||||
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||||
found = True
|
found = True
|
||||||
|
break
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
@ -96,7 +64,7 @@ def _fill_oldctls(prefix):
|
|||||||
|
|
||||||
def _sysctl_set(name, val):
|
def _sysctl_set(name, val):
|
||||||
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
debug1('>> %s' % ' '.join(argv))
|
||||||
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
|
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
|
||||||
# No env: No output. (Or error that won't be parsed.)
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
|
|
||||||
@ -111,13 +79,13 @@ def sysctl_set(name, val, permanent=False):
|
|||||||
if not _oldctls:
|
if not _oldctls:
|
||||||
_fill_oldctls(PREFIX)
|
_fill_oldctls(PREFIX)
|
||||||
if not (name in _oldctls):
|
if not (name in _oldctls):
|
||||||
debug1('>> No such sysctl: %r\n' % name)
|
debug1('>> No such sysctl: %r' % name)
|
||||||
return False
|
return False
|
||||||
oldval = _oldctls[name]
|
oldval = _oldctls[name]
|
||||||
if val != oldval:
|
if val != oldval:
|
||||||
rv = _sysctl_set(name, val)
|
rv = _sysctl_set(name, val)
|
||||||
if rv == 0 and permanent:
|
if rv == 0 and permanent:
|
||||||
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
|
debug1('>> ...saving permanently in /etc/sysctl.conf')
|
||||||
f = open('/etc/sysctl.conf', 'a')
|
f = open('/etc/sysctl.conf', 'a')
|
||||||
f.write('\n'
|
f.write('\n'
|
||||||
'# Added by sshuttle\n'
|
'# Added by sshuttle\n'
|
||||||
@ -130,7 +98,7 @@ def sysctl_set(name, val, permanent=False):
|
|||||||
|
|
||||||
def ipfw(*args):
|
def ipfw(*args):
|
||||||
argv = ['ipfw', '-q'] + list(args)
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
debug1('>> %s' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv, env=get_env())
|
rv = ssubprocess.call(argv, env=get_env())
|
||||||
# No env: No output. (Or error that won't be parsed.)
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
if rv:
|
if rv:
|
||||||
@ -139,7 +107,7 @@ def ipfw(*args):
|
|||||||
|
|
||||||
def ipfw_noexit(*args):
|
def ipfw_noexit(*args):
|
||||||
argv = ['ipfw', '-q'] + list(args)
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
debug1('>> %s' % ' '.join(argv))
|
||||||
ssubprocess.call(argv, env=get_env())
|
ssubprocess.call(argv, env=get_env())
|
||||||
# No env: No output. (Or error that won't be parsed.)
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
|
|
||||||
@ -161,7 +129,7 @@ class Method(BaseMethod):
|
|||||||
if not dstip:
|
if not dstip:
|
||||||
debug1(
|
debug1(
|
||||||
"-- ignored UDP from %r: "
|
"-- ignored UDP from %r: "
|
||||||
"couldn't determine destination IP address\n" % (srcip,))
|
"couldn't determine destination IP address" % (srcip,))
|
||||||
return None
|
return None
|
||||||
return srcip, dstip, data
|
return srcip, dstip, data
|
||||||
|
|
||||||
@ -169,15 +137,14 @@ class Method(BaseMethod):
|
|||||||
if not srcip:
|
if not srcip:
|
||||||
debug1(
|
debug1(
|
||||||
"-- ignored UDP to %r: "
|
"-- ignored UDP to %r: "
|
||||||
"couldn't determine source IP address\n" % (dstip,))
|
"couldn't determine source IP address" % (dstip,))
|
||||||
return
|
return
|
||||||
|
|
||||||
# debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
|
# debug3('Sending SRC: %r DST: %r' % (srcip, dstip))
|
||||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||||
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||||
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
|
|
||||||
sender.bind(srcip)
|
sender.bind(srcip)
|
||||||
sender.sendto(data, dstip)
|
sender.sendto(data, dstip)
|
||||||
sender.close()
|
sender.close()
|
||||||
@ -189,7 +156,7 @@ class Method(BaseMethod):
|
|||||||
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user):
|
user, tmark):
|
||||||
# IPv6 not supported
|
# IPv6 not supported
|
||||||
if family not in [socket.AF_INET]:
|
if family not in [socket.AF_INET]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
@ -207,8 +174,7 @@ class Method(BaseMethod):
|
|||||||
if subnets or dnsport:
|
if subnets or dnsport:
|
||||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||||
|
|
||||||
ipfw('add', '1', 'check-state', 'ip',
|
ipfw('add', '1', 'check-state', ':sshuttle')
|
||||||
'from', 'any', 'to', 'any')
|
|
||||||
|
|
||||||
ipfw('add', '1', 'skipto', '2',
|
ipfw('add', '1', 'skipto', '2',
|
||||||
'tcp',
|
'tcp',
|
||||||
@ -216,7 +182,7 @@ class Method(BaseMethod):
|
|||||||
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
|
||||||
'tcp',
|
'tcp',
|
||||||
'from', 'any', 'to', 'table(126)',
|
'from', 'any', 'to', 'table(126)',
|
||||||
'not', 'ipttl', '63', 'keep-state', 'setup')
|
'setup', 'keep-state', ':sshuttle')
|
||||||
|
|
||||||
ipfw_noexit('table', '124', 'flush')
|
ipfw_noexit('table', '124', 'flush')
|
||||||
dnscount = 0
|
dnscount = 0
|
||||||
@ -227,26 +193,24 @@ class Method(BaseMethod):
|
|||||||
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
|
||||||
'udp',
|
'udp',
|
||||||
'from', 'any', 'to', 'table(124)',
|
'from', 'any', 'to', 'table(124)',
|
||||||
'not', 'ipttl', '63')
|
'keep-state', ':sshuttle')
|
||||||
ipfw('add', '1', 'allow',
|
ipfw('add', '1', 'allow',
|
||||||
'udp',
|
'udp',
|
||||||
'from', 'any', 'to', 'any',
|
'from', 'any', 'to', 'any')
|
||||||
'ipttl', '63')
|
|
||||||
|
|
||||||
if subnets:
|
if subnets:
|
||||||
# create new subnet entries
|
# create new subnet entries
|
||||||
for _, swidth, sexclude, snet in sorted(subnets,
|
for _, swidth, sexclude, snet, fport, lport \
|
||||||
key=lambda s: s[1],
|
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||||
reverse=True):
|
|
||||||
if sexclude:
|
if sexclude:
|
||||||
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
||||||
else:
|
else:
|
||||||
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
|
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
if family not in [socket.AF_INET]:
|
if family not in [socket.AF_INET]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by tproxy method'
|
'Address family "%s" unsupported by ipfw method'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
|
|
||||||
ipfw_noexit('delete', '1')
|
ipfw_noexit('delete', '1')
|
||||||
@ -258,5 +222,5 @@ class Method(BaseMethod):
|
|||||||
if which("ipfw"):
|
if which("ipfw"):
|
||||||
return True
|
return True
|
||||||
debug2("ipfw method not supported because 'ipfw' command is "
|
debug2("ipfw method not supported because 'ipfw' command is "
|
||||||
"missing.\n")
|
"missing.")
|
||||||
return False
|
return False
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import socket
|
import socket
|
||||||
from sshuttle.firewall import subnet_weight
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import family_to_string, which, debug2
|
from sshuttle.helpers import family_to_string, which, debug2
|
||||||
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
|
from sshuttle.linux import ipt, ipt_chain_exists, nonfatal
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
@ -13,23 +13,18 @@ class Method(BaseMethod):
|
|||||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
# "-A OUTPUT").
|
# "-A OUTPUT").
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user):
|
user, tmark):
|
||||||
# only ipv4 supported with NAT
|
if family != socket.AF_INET and family != socket.AF_INET6:
|
||||||
if family != socket.AF_INET:
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by nat method_name'
|
'Address family "%s" unsupported by nat method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by nat method_name")
|
raise Exception("UDP not supported by nat method_name")
|
||||||
|
|
||||||
table = "nat"
|
table = "nat"
|
||||||
|
|
||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
def _ipt_ttl(*args):
|
|
||||||
return ipt_ttl(family, table, *args)
|
|
||||||
|
|
||||||
def _ipm(*args):
|
def _ipm(*args):
|
||||||
return ipt(family, "mangle", *args)
|
return ipt(family, "mangle", *args)
|
||||||
|
|
||||||
@ -50,16 +45,11 @@ class Method(BaseMethod):
|
|||||||
_ipt('-I', 'OUTPUT', '1', *args)
|
_ipt('-I', 'OUTPUT', '1', *args)
|
||||||
_ipt('-I', 'PREROUTING', '1', *args)
|
_ipt('-I', 'PREROUTING', '1', *args)
|
||||||
|
|
||||||
# This TTL hack allows the client and server to run on the
|
|
||||||
# same host. The connections the sshuttle server makes will
|
|
||||||
# have TTL set to 63.
|
|
||||||
_ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', '63')
|
|
||||||
|
|
||||||
# Redirect DNS traffic as requested. This includes routing traffic
|
# Redirect DNS traffic as requested. This includes routing traffic
|
||||||
# to localhost DNS servers through sshuttle.
|
# to localhost DNS servers through sshuttle.
|
||||||
for _, ip in [i for i in nslist if i[0] == family]:
|
for _, ip in [i for i in nslist if i[0] == family]:
|
||||||
_ipt('-A', chain, '-j', 'REDIRECT',
|
_ipt('-A', chain, '-j', 'REDIRECT',
|
||||||
'--dest', '%s/32' % ip,
|
'--dest', '%s' % ip,
|
||||||
'-p', 'udp',
|
'-p', 'udp',
|
||||||
'--dport', '53',
|
'--dport', '53',
|
||||||
'--to-ports', str(dnsport))
|
'--to-ports', str(dnsport))
|
||||||
@ -87,7 +77,7 @@ class Method(BaseMethod):
|
|||||||
|
|
||||||
def restore_firewall(self, port, family, udp, user):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
# only ipv4 supported with NAT
|
# only ipv4 supported with NAT
|
||||||
if family != socket.AF_INET:
|
if family != socket.AF_INET and family != socket.AF_INET6:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by nat method_name'
|
'Address family "%s" unsupported by nat method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
@ -99,9 +89,6 @@ class Method(BaseMethod):
|
|||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
def _ipt_ttl(*args):
|
|
||||||
return ipt_ttl(family, table, *args)
|
|
||||||
|
|
||||||
def _ipm(*args):
|
def _ipm(*args):
|
||||||
return ipt(family, "mangle", *args)
|
return ipt(family, "mangle", *args)
|
||||||
|
|
||||||
@ -123,11 +110,12 @@ class Method(BaseMethod):
|
|||||||
def get_supported_features(self):
|
def get_supported_features(self):
|
||||||
result = super(Method, self).get_supported_features()
|
result = super(Method, self).get_supported_features()
|
||||||
result.user = True
|
result.user = True
|
||||||
|
result.ipv6 = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def is_supported(self):
|
def is_supported(self):
|
||||||
if which("iptables"):
|
if which("iptables"):
|
||||||
return True
|
return True
|
||||||
debug2("nat method not supported because 'iptables' command "
|
debug2("nat method not supported because 'iptables' command "
|
||||||
"is missing.\n")
|
"is missing.")
|
||||||
return False
|
return False
|
||||||
|
@ -13,7 +13,7 @@ class Method(BaseMethod):
|
|||||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
# "-A OUTPUT").
|
# "-A OUTPUT").
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user):
|
user, tmark):
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by nft")
|
raise Exception("UDP not supported by nft")
|
||||||
|
|
||||||
@ -45,14 +45,6 @@ class Method(BaseMethod):
|
|||||||
else:
|
else:
|
||||||
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return')
|
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return')
|
||||||
|
|
||||||
# This TTL hack allows the client and server to run on the
|
|
||||||
# same host. The connections the sshuttle server makes will
|
|
||||||
# have TTL set to 63.
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
_nft('add rule', chain, 'ip ttl == 63 return')
|
|
||||||
elif family == socket.AF_INET6:
|
|
||||||
_nft('add rule', chain, 'ip6 hoplimit == 63 return')
|
|
||||||
|
|
||||||
# Strings to use below to simplify our code
|
# Strings to use below to simplify our code
|
||||||
if family == socket.AF_INET:
|
if family == socket.AF_INET:
|
||||||
ip_version_l = 'ipv4'
|
ip_version_l = 'ipv4'
|
||||||
@ -118,5 +110,5 @@ class Method(BaseMethod):
|
|||||||
def is_supported(self):
|
def is_supported(self):
|
||||||
if which("nft"):
|
if which("nft"):
|
||||||
return True
|
return True
|
||||||
debug2("nft method not supported because 'nft' command is missing.\n")
|
debug2("nft method not supported because 'nft' command is missing.")
|
||||||
return False
|
return False
|
||||||
|
@ -11,8 +11,8 @@ from fcntl import ioctl
|
|||||||
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
||||||
sizeof, addressof, memmove
|
sizeof, addressof, memmove
|
||||||
from sshuttle.firewall import subnet_weight
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string, \
|
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, \
|
||||||
get_env, which
|
family_to_string, get_env, which
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
@ -273,7 +273,7 @@ class OpenBsd(Generic):
|
|||||||
def add_anchors(self, anchor):
|
def add_anchors(self, anchor):
|
||||||
# before adding anchors and rules we must override the skip lo
|
# before adding anchors and rules we must override the skip lo
|
||||||
# that comes by default in openbsd pf.conf so the rules we will add,
|
# that comes by default in openbsd pf.conf so the rules we will add,
|
||||||
# which rely on translating/filtering packets on lo, can work
|
# which rely on translating/filtering packets on lo, can work
|
||||||
if self.has_skip_loopback():
|
if self.has_skip_loopback():
|
||||||
pfctl('-f /dev/stdin', b'match on lo\n')
|
pfctl('-f /dev/stdin', b'match on lo\n')
|
||||||
super(OpenBsd, self).add_anchors(anchor)
|
super(OpenBsd, self).add_anchors(anchor)
|
||||||
@ -353,7 +353,7 @@ class Darwin(FreeBsd):
|
|||||||
def add_anchors(self, anchor):
|
def add_anchors(self, anchor):
|
||||||
# before adding anchors and rules we must override the skip lo
|
# before adding anchors and rules we must override the skip lo
|
||||||
# that in some cases ends up in the chain so the rules we will add,
|
# that in some cases ends up in the chain so the rules we will add,
|
||||||
# which rely on translating/filtering packets on lo, can work
|
# which rely on translating/filtering packets on lo, can work
|
||||||
if self.has_skip_loopback():
|
if self.has_skip_loopback():
|
||||||
pfctl('-f /dev/stdin', b'pass on lo\n')
|
pfctl('-f /dev/stdin', b'pass on lo\n')
|
||||||
super(Darwin, self).add_anchors(anchor)
|
super(Darwin, self).add_anchors(anchor)
|
||||||
@ -386,13 +386,17 @@ else:
|
|||||||
|
|
||||||
def pfctl(args, stdin=None):
|
def pfctl(args, stdin=None):
|
||||||
argv = ['pfctl'] + shlex.split(args)
|
argv = ['pfctl'] + shlex.split(args)
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
debug1('>> %s' % ' '.join(argv))
|
||||||
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
||||||
stdout=ssubprocess.PIPE,
|
stdout=ssubprocess.PIPE,
|
||||||
stderr=ssubprocess.PIPE,
|
stderr=ssubprocess.PIPE,
|
||||||
env=get_env())
|
env=get_env())
|
||||||
o = p.communicate(stdin)
|
o = p.communicate(stdin)
|
||||||
if p.returncode:
|
if p.returncode:
|
||||||
|
log('%r returned %d, stdout and stderr follows: ' %
|
||||||
|
(argv, p.returncode))
|
||||||
|
log("stdout:\n%s" % o[0].decode("ascii"))
|
||||||
|
log("stderr:\n%s" % o[1].decode("ascii"))
|
||||||
raise Fatal('%r returned %d' % (argv, p.returncode))
|
raise Fatal('%r returned %d' % (argv, p.returncode))
|
||||||
|
|
||||||
return o
|
return o
|
||||||
@ -444,7 +448,7 @@ class Method(BaseMethod):
|
|||||||
return sock.getsockname()
|
return sock.getsockname()
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user):
|
user, tmark):
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by pf method_name'
|
'Address family "%s" unsupported by pf method_name'
|
||||||
@ -495,5 +499,5 @@ class Method(BaseMethod):
|
|||||||
def is_supported(self):
|
def is_supported(self):
|
||||||
if which("pfctl"):
|
if which("pfctl"):
|
||||||
return True
|
return True
|
||||||
debug2("pf method not supported because 'pfctl' command is missing.\n")
|
debug2("pf method not supported because 'pfctl' command is missing.")
|
||||||
return False
|
return False
|
||||||
|
@ -1,26 +1,12 @@
|
|||||||
import struct
|
import struct
|
||||||
from sshuttle.firewall import subnet_weight
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import family_to_string
|
from sshuttle.helpers import family_to_string
|
||||||
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
|
from sshuttle.linux import ipt, ipt_chain_exists
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
from sshuttle.helpers import debug1, debug2, debug3, Fatal, which
|
from sshuttle.helpers import debug1, debug2, debug3, Fatal, which
|
||||||
|
|
||||||
recvmsg = None
|
import socket
|
||||||
try:
|
import os
|
||||||
# try getting recvmsg from python
|
|
||||||
import socket as pythonsocket
|
|
||||||
getattr(pythonsocket.socket, "recvmsg")
|
|
||||||
socket = pythonsocket
|
|
||||||
recvmsg = "python"
|
|
||||||
except AttributeError:
|
|
||||||
# try getting recvmsg from socket_ext library
|
|
||||||
try:
|
|
||||||
import socket_ext
|
|
||||||
getattr(socket_ext.socket, "recvmsg")
|
|
||||||
socket = socket_ext
|
|
||||||
recvmsg = "socket_ext"
|
|
||||||
except ImportError:
|
|
||||||
import socket
|
|
||||||
|
|
||||||
|
|
||||||
IP_TRANSPARENT = 19
|
IP_TRANSPARENT = 19
|
||||||
@ -30,75 +16,37 @@ SOL_IPV6 = 41
|
|||||||
IPV6_ORIGDSTADDR = 74
|
IPV6_ORIGDSTADDR = 74
|
||||||
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
||||||
|
|
||||||
if recvmsg == "python":
|
|
||||||
def recv_udp(listener, bufsize):
|
def recv_udp(listener, bufsize):
|
||||||
debug3('Accept UDP python using recvmsg.\n')
|
debug3('Accept UDP python using recvmsg.')
|
||||||
data, ancdata, _, srcip = listener.recvmsg(
|
data, ancdata, _, srcip = listener.recvmsg(
|
||||||
4096, socket.CMSG_SPACE(24))
|
4096, socket.CMSG_SPACE(24))
|
||||||
dstip = None
|
dstip = None
|
||||||
family = None
|
family = None
|
||||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
|
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
|
||||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||||
port = socket.htons(port)
|
port = socket.htons(port)
|
||||||
if family == socket.AF_INET:
|
if family == socket.AF_INET:
|
||||||
start = 4
|
start = 4
|
||||||
length = 4
|
length = 4
|
||||||
else:
|
else:
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||||
dstip = (ip, port)
|
dstip = (ip, port)
|
||||||
break
|
break
|
||||||
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
|
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
|
||||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||||
port = socket.htons(port)
|
port = socket.htons(port)
|
||||||
if family == socket.AF_INET6:
|
if family == socket.AF_INET6:
|
||||||
start = 8
|
start = 8
|
||||||
length = 16
|
length = 16
|
||||||
else:
|
else:
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||||
dstip = (ip, port)
|
dstip = (ip, port)
|
||||||
break
|
break
|
||||||
return (srcip, dstip, data)
|
return (srcip, dstip, data)
|
||||||
elif recvmsg == "socket_ext":
|
|
||||||
def recv_udp(listener, bufsize):
|
|
||||||
debug3('Accept UDP using socket_ext recvmsg.\n')
|
|
||||||
srcip, data, adata, _ = listener.recvmsg(
|
|
||||||
(bufsize,), socket.CMSG_SPACE(24))
|
|
||||||
dstip = None
|
|
||||||
family = None
|
|
||||||
for a in adata:
|
|
||||||
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
|
|
||||||
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
|
||||||
port = socket.htons(port)
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
start = 4
|
|
||||||
length = 4
|
|
||||||
else:
|
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
|
||||||
ip = socket.inet_ntop(
|
|
||||||
family, a.cmsg_data[start:start + length])
|
|
||||||
dstip = (ip, port)
|
|
||||||
break
|
|
||||||
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
|
|
||||||
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
|
||||||
port = socket.htons(port)
|
|
||||||
if family == socket.AF_INET6:
|
|
||||||
start = 8
|
|
||||||
length = 16
|
|
||||||
else:
|
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
|
||||||
ip = socket.inet_ntop(
|
|
||||||
family, a.cmsg_data[start:start + length])
|
|
||||||
dstip = (ip, port)
|
|
||||||
break
|
|
||||||
return (srcip, dstip, data[0])
|
|
||||||
else:
|
|
||||||
def recv_udp(listener, bufsize):
|
|
||||||
debug3('Accept UDP using recvfrom.\n')
|
|
||||||
data, srcip = listener.recvfrom(bufsize)
|
|
||||||
return (srcip, None, data)
|
|
||||||
|
|
||||||
|
|
||||||
class Method(BaseMethod):
|
class Method(BaseMethod):
|
||||||
@ -106,12 +54,8 @@ class Method(BaseMethod):
|
|||||||
def get_supported_features(self):
|
def get_supported_features(self):
|
||||||
result = super(Method, self).get_supported_features()
|
result = super(Method, self).get_supported_features()
|
||||||
result.ipv6 = True
|
result.ipv6 = True
|
||||||
if recvmsg is None:
|
result.udp = True
|
||||||
result.udp = False
|
result.dns = True
|
||||||
result.dns = False
|
|
||||||
else:
|
|
||||||
result.udp = True
|
|
||||||
result.dns = True
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_tcp_dstip(self, sock):
|
def get_tcp_dstip(self, sock):
|
||||||
@ -126,6 +70,15 @@ class Method(BaseMethod):
|
|||||||
return None
|
return None
|
||||||
return srcip, dstip, data
|
return srcip, dstip, data
|
||||||
|
|
||||||
|
def setsockopt_error(self, e):
|
||||||
|
"""The tproxy method needs root permissions to successfully
|
||||||
|
set the IP_TRANSPARENT option on sockets. This method is
|
||||||
|
called when we receive a PermissionError when trying to do
|
||||||
|
so."""
|
||||||
|
raise Fatal("Insufficient permissions for tproxy method.\n"
|
||||||
|
"Your effective UID is %d, not 0. Try rerunning as root.\n"
|
||||||
|
% os.geteuid())
|
||||||
|
|
||||||
def send_udp(self, sock, srcip, dstip, data):
|
def send_udp(self, sock, srcip, dstip, data):
|
||||||
if not srcip:
|
if not srcip:
|
||||||
debug1(
|
debug1(
|
||||||
@ -134,16 +87,26 @@ class Method(BaseMethod):
|
|||||||
return
|
return
|
||||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
try:
|
||||||
|
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
|
except PermissionError as e:
|
||||||
|
self.setsockopt_error(e)
|
||||||
sender.bind(srcip)
|
sender.bind(srcip)
|
||||||
sender.sendto(data, dstip)
|
sender.sendto(data, dstip)
|
||||||
sender.close()
|
sender.close()
|
||||||
|
|
||||||
def setup_tcp_listener(self, tcp_listener):
|
def setup_tcp_listener(self, tcp_listener):
|
||||||
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
try:
|
||||||
|
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
|
except PermissionError as e:
|
||||||
|
self.setsockopt_error(e)
|
||||||
|
|
||||||
def setup_udp_listener(self, udp_listener):
|
def setup_udp_listener(self, udp_listener):
|
||||||
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
try:
|
||||||
|
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
|
except PermissionError as e:
|
||||||
|
self.setsockopt_error(e)
|
||||||
|
|
||||||
if udp_listener.v4 is not None:
|
if udp_listener.v4 is not None:
|
||||||
udp_listener.v4.setsockopt(
|
udp_listener.v4.setsockopt(
|
||||||
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
||||||
@ -151,17 +114,7 @@ class Method(BaseMethod):
|
|||||||
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
user):
|
user, tmark):
|
||||||
if self.firewall is None:
|
|
||||||
tmark = '1'
|
|
||||||
else:
|
|
||||||
tmark = self.firewall.tmark
|
|
||||||
|
|
||||||
self.setup_firewall_tproxy(port, dnsport, nslist, family, subnets, udp,
|
|
||||||
user, tmark)
|
|
||||||
|
|
||||||
def setup_firewall_tproxy(self, port, dnsport, nslist, family, subnets,
|
|
||||||
udp, user, tmark):
|
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by tproxy method'
|
'Address family "%s" unsupported by tproxy method'
|
||||||
@ -172,9 +125,6 @@ class Method(BaseMethod):
|
|||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
def _ipt_ttl(*args):
|
|
||||||
return ipt_ttl(family, table, *args)
|
|
||||||
|
|
||||||
def _ipt_proto_ports(proto, fport, lport):
|
def _ipt_proto_ports(proto, fport, lport):
|
||||||
return proto + ('--dport', '%d:%d' % (fport, lport)) \
|
return proto + ('--dport', '%d:%d' % (fport, lport)) \
|
||||||
if fport else proto
|
if fport else proto
|
||||||
@ -192,8 +142,24 @@ class Method(BaseMethod):
|
|||||||
_ipt('-F', divert_chain)
|
_ipt('-F', divert_chain)
|
||||||
_ipt('-N', tproxy_chain)
|
_ipt('-N', tproxy_chain)
|
||||||
_ipt('-F', tproxy_chain)
|
_ipt('-F', tproxy_chain)
|
||||||
_ipt('-I', 'OUTPUT', tmark, '-j', mark_chain)
|
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
|
||||||
_ipt('-I', 'PREROUTING', tmark, '-j', tproxy_chain)
|
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
|
||||||
|
|
||||||
|
# Don't have packets sent to any of our local IP addresses go
|
||||||
|
# through the tproxy or mark chains.
|
||||||
|
#
|
||||||
|
# Without this fix, if a large subnet is redirected through
|
||||||
|
# sshuttle (i.e., 0/0), then the user may be unable to receive
|
||||||
|
# UDP responses or connect to their own machine using an IP
|
||||||
|
# besides (127.0.0.1). Prior to including these lines, the
|
||||||
|
# documentation reminded the user to use -x to exclude their
|
||||||
|
# own IP addresses to receive UDP responses if they are
|
||||||
|
# redirecting a large subnet through sshuttle (i.e., 0/0).
|
||||||
|
_ipt('-A', tproxy_chain, '-j', 'RETURN', '-m', 'addrtype',
|
||||||
|
'--dst-type', 'LOCAL')
|
||||||
|
_ipt('-A', mark_chain, '-j', 'RETURN', '-m', 'addrtype',
|
||||||
|
'--dst-type', 'LOCAL')
|
||||||
|
|
||||||
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', tmark)
|
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', tmark)
|
||||||
_ipt('-A', divert_chain, '-j', 'ACCEPT')
|
_ipt('-A', divert_chain, '-j', 'ACCEPT')
|
||||||
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||||
@ -208,7 +174,7 @@ class Method(BaseMethod):
|
|||||||
'--dest', '%s/32' % ip,
|
'--dest', '%s/32' % ip,
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
||||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x'+tmark+'/0x'+tmark,
|
'--tproxy-mark', tmark,
|
||||||
'--dest', '%s/32' % ip,
|
'--dest', '%s/32' % ip,
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
||||||
'--on-port', str(dnsport))
|
'--on-port', str(dnsport))
|
||||||
@ -233,7 +199,7 @@ class Method(BaseMethod):
|
|||||||
'-m', 'tcp',
|
'-m', 'tcp',
|
||||||
*tcp_ports)
|
*tcp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x'+tmark+'/0x'+tmark,
|
'--tproxy-mark', tmark,
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'tcp',
|
'-m', 'tcp',
|
||||||
*(tcp_ports + ('--on-port', str(port))))
|
*(tcp_ports + ('--on-port', str(port))))
|
||||||
@ -257,7 +223,7 @@ class Method(BaseMethod):
|
|||||||
'-m', 'udp',
|
'-m', 'udp',
|
||||||
*udp_ports)
|
*udp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x'+tmark+'/0x'+tmark,
|
'--tproxy-mark', tmark,
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'udp',
|
'-m', 'udp',
|
||||||
*(udp_ports + ('--on-port', str(port))))
|
*(udp_ports + ('--on-port', str(port))))
|
||||||
@ -273,9 +239,6 @@ class Method(BaseMethod):
|
|||||||
def _ipt(*args):
|
def _ipt(*args):
|
||||||
return ipt(family, table, *args)
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
def _ipt_ttl(*args):
|
|
||||||
return ipt_ttl(family, table, *args)
|
|
||||||
|
|
||||||
mark_chain = 'sshuttle-m-%s' % port
|
mark_chain = 'sshuttle-m-%s' % port
|
||||||
tproxy_chain = 'sshuttle-t-%s' % port
|
tproxy_chain = 'sshuttle-t-%s' % port
|
||||||
divert_chain = 'sshuttle-d-%s' % port
|
divert_chain = 'sshuttle-d-%s' % port
|
||||||
|
@ -132,6 +132,7 @@ def parse_ipport(s):
|
|||||||
|
|
||||||
|
|
||||||
def parse_list(lst):
|
def parse_list(lst):
|
||||||
|
"""Parse a comma separated string into a list."""
|
||||||
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
||||||
|
|
||||||
|
|
||||||
@ -146,9 +147,33 @@ class Concat(Action):
|
|||||||
setattr(namespace, self.dest, curr_value + values)
|
setattr(namespace, self.dest, curr_value + values)
|
||||||
|
|
||||||
|
|
||||||
parser = ArgumentParser(
|
# Override one function in the ArgumentParser so that we can have
|
||||||
|
# better control for how we parse files containing arguments. We
|
||||||
|
# expect one argument per line, but strip whitespace/quotes from the
|
||||||
|
# beginning/end of the lines.
|
||||||
|
class MyArgumentParser(ArgumentParser):
|
||||||
|
def convert_arg_line_to_args(self, arg_line):
|
||||||
|
# Ignore comments
|
||||||
|
if arg_line.startswith("#"):
|
||||||
|
return []
|
||||||
|
|
||||||
|
# strip whitespace at beginning and end of line
|
||||||
|
arg_line = arg_line.strip()
|
||||||
|
|
||||||
|
# When copying parameters from the command line to a file,
|
||||||
|
# some users might copy the quotes they used on the command
|
||||||
|
# line into the config file. We ignore these if the line
|
||||||
|
# starts and ends with the same quote.
|
||||||
|
if arg_line.startswith("'") and arg_line.endswith("'") or \
|
||||||
|
arg_line.startswith('"') and arg_line.endswith('"'):
|
||||||
|
arg_line = arg_line[1:-1]
|
||||||
|
|
||||||
|
return [arg_line]
|
||||||
|
|
||||||
|
|
||||||
|
parser = MyArgumentParser(
|
||||||
prog="sshuttle",
|
prog="sshuttle",
|
||||||
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
|
usage="%(prog)s [-l [ip:]port] -r [user@]sshserver[:port] <subnets...>",
|
||||||
fromfile_prefix_chars="@"
|
fromfile_prefix_chars="@"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -196,6 +221,7 @@ parser.add_argument(
|
|||||||
type=parse_list,
|
type=parse_list,
|
||||||
help="""
|
help="""
|
||||||
capture and forward DNS requests made to the following servers
|
capture and forward DNS requests made to the following servers
|
||||||
|
(comma separated)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -256,7 +282,7 @@ parser.add_argument(
|
|||||||
action="count",
|
action="count",
|
||||||
default=0,
|
default=0,
|
||||||
help="""
|
help="""
|
||||||
increase debug message verbosity
|
increase debug message verbosity (can be used more than once)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -412,8 +438,9 @@ parser.add_argument(
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-t", "--tmark",
|
"-t", "--tmark",
|
||||||
metavar="[MARK]",
|
metavar="[MARK]",
|
||||||
default="1",
|
default="0x01",
|
||||||
help="""
|
help="""
|
||||||
transproxy optional traffic mark with provided MARK value
|
tproxy optional traffic mark with provided MARK value in
|
||||||
|
hexadecimal (default '0x01')
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,7 @@ def _notify(message):
|
|||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
debug1("Error creating socket to notify systemd: %s\n" % e)
|
debug1("Error creating socket to notify systemd: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
@ -37,7 +37,7 @@ def _notify(message):
|
|||||||
try:
|
try:
|
||||||
return (sock.sendto(message, addr) > 0)
|
return (sock.sendto(message, addr) > 0)
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
debug1("Error notifying systemd: %s\n" % e)
|
debug1("Error notifying systemd: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import traceback
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import platform
|
|
||||||
|
|
||||||
|
|
||||||
import sshuttle.ssnet as ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
@ -96,7 +95,7 @@ def _list_routes(argv, extract_route):
|
|||||||
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
|
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
if rv != 0:
|
if rv != 0:
|
||||||
log('WARNING: %r returned %d\n' % (argv, rv))
|
log('WARNING: %r returned %d' % (argv, rv))
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
@ -108,7 +107,7 @@ def list_routes():
|
|||||||
routes = _list_routes(['netstat', '-rn'], _route_netstat)
|
routes = _list_routes(['netstat', '-rn'], _route_netstat)
|
||||||
else:
|
else:
|
||||||
log('WARNING: Neither "ip" nor "netstat" were found on the server. '
|
log('WARNING: Neither "ip" nor "netstat" were found on the server. '
|
||||||
'--auto-nets feature will not work.\n')
|
'--auto-nets feature will not work.')
|
||||||
routes = []
|
routes = []
|
||||||
|
|
||||||
for (family, ip, width) in routes:
|
for (family, ip, width) in routes:
|
||||||
@ -135,7 +134,7 @@ def start_hostwatch(seed_hosts, auto_hosts):
|
|||||||
s1.close()
|
s1.close()
|
||||||
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
|
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
log('%s\n' % _exc_dump())
|
log('%s' % _exc_dump())
|
||||||
rv = 98
|
rv = 98
|
||||||
finally:
|
finally:
|
||||||
os._exit(rv)
|
os._exit(rv)
|
||||||
@ -191,12 +190,11 @@ class DnsProxy(Handler):
|
|||||||
|
|
||||||
family, sockaddr = self._addrinfo(peer, port)
|
family, sockaddr = self._addrinfo(peer, port)
|
||||||
sock = socket.socket(family, socket.SOCK_DGRAM)
|
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||||
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
|
|
||||||
sock.connect(sockaddr)
|
sock.connect(sockaddr)
|
||||||
|
|
||||||
self.peers[sock] = peer
|
self.peers[sock] = peer
|
||||||
|
|
||||||
debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries))
|
debug2('DNS: sending to %r:%d (try %d)' % (peer, port, self.tries))
|
||||||
try:
|
try:
|
||||||
sock.send(self.request)
|
sock.send(self.request)
|
||||||
self.socks.append(sock)
|
self.socks.append(sock)
|
||||||
@ -206,11 +204,11 @@ class DnsProxy(Handler):
|
|||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
# and sometimes by send(). We have to catch both.
|
# and sometimes by send(). We have to catch both.
|
||||||
debug2('DNS send to %r: %s\n' % (peer, e))
|
debug2('DNS send to %r: %s' % (peer, e))
|
||||||
self.try_send()
|
self.try_send()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
log('DNS send to %r: %s\n' % (peer, e))
|
log('DNS send to %r: %s' % (peer, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
@ -227,13 +225,13 @@ class DnsProxy(Handler):
|
|||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
# and sometimes by send(). We have to catch both.
|
# and sometimes by send(). We have to catch both.
|
||||||
debug2('DNS recv from %r: %s\n' % (peer, e))
|
debug2('DNS recv from %r: %s' % (peer, e))
|
||||||
self.try_send()
|
self.try_send()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
log('DNS recv from %r: %s\n' % (peer, e))
|
log('DNS recv from %r: %s' % (peer, e))
|
||||||
return
|
return
|
||||||
debug2('DNS response: %d bytes\n' % len(data))
|
debug2('DNS response: %d bytes' % len(data))
|
||||||
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
||||||
self.ok = False
|
self.ok = False
|
||||||
|
|
||||||
@ -247,16 +245,14 @@ class UdpProxy(Handler):
|
|||||||
self.mux = mux
|
self.mux = mux
|
||||||
self.chan = chan
|
self.chan = chan
|
||||||
self.sock = sock
|
self.sock = sock
|
||||||
if family == socket.AF_INET:
|
|
||||||
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
|
|
||||||
|
|
||||||
def send(self, dstip, data):
|
def send(self, dstip, data):
|
||||||
debug2(' s: UDP: sending to %r port %d\n' % dstip)
|
debug2('UDP: sending to %r port %d' % dstip)
|
||||||
try:
|
try:
|
||||||
self.sock.sendto(data, dstip)
|
self.sock.sendto(data, dstip)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log(' s: UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
|
log('UDP send to %r port %d: %s' % (dstip[0], dstip[1], e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
@ -264,147 +260,157 @@ class UdpProxy(Handler):
|
|||||||
data, peer = sock.recvfrom(4096)
|
data, peer = sock.recvfrom(4096)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
log(' s: UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
|
log('UDP recv from %r port %d: %s' % (peer[0], peer[1], e))
|
||||||
return
|
return
|
||||||
debug2(' s: UDP response: %d bytes\n' % len(data))
|
debug2('UDP response: %d bytes' % len(data))
|
||||||
hdr = b("%s,%r," % (peer[0], peer[1]))
|
hdr = b("%s,%r," % (peer[0], peer[1]))
|
||||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||||
|
|
||||||
|
|
||||||
def main(latency_control, auto_hosts, to_nameserver, auto_nets):
|
def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
|
||||||
debug1(' s: Starting server with Python version %s\n'
|
auto_nets):
|
||||||
% platform.python_version())
|
try:
|
||||||
|
helpers.logprefix = ' s: '
|
||||||
|
|
||||||
helpers.logprefix = ' s: '
|
debug1('latency control setting = %r' % latency_control)
|
||||||
debug1('latency control setting = %r\n' % latency_control)
|
if latency_buffer_size:
|
||||||
|
import sshuttle.ssnet as ssnet
|
||||||
|
ssnet.LATENCY_BUFFER_SIZE = latency_buffer_size
|
||||||
|
|
||||||
# synchronization header
|
# synchronization header
|
||||||
sys.stdout.write('\0\0SSHUTTLE0001')
|
sys.stdout.write('\0\0SSHUTTLE0001')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
mux = Mux(sys.stdin, sys.stdout)
|
mux = Mux(sys.stdin, sys.stdout)
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
|
|
||||||
debug1('auto-nets:' + str(auto_nets) + '\n')
|
debug1('auto-nets:' + str(auto_nets))
|
||||||
if auto_nets:
|
if auto_nets:
|
||||||
routes = list(list_routes())
|
routes = list(list_routes())
|
||||||
debug1('available routes:\n')
|
debug1('available routes:')
|
||||||
|
for r in routes:
|
||||||
|
debug1(' %d/%s/%d' % r)
|
||||||
|
else:
|
||||||
|
routes = []
|
||||||
|
|
||||||
|
routepkt = ''
|
||||||
for r in routes:
|
for r in routes:
|
||||||
debug1(' %d/%s/%d\n' % r)
|
routepkt += '%d,%s,%d\n' % r
|
||||||
else:
|
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
|
||||||
routes = []
|
|
||||||
|
|
||||||
routepkt = ''
|
hw = Hostwatch()
|
||||||
for r in routes:
|
hw.leftover = b('')
|
||||||
routepkt += '%d,%s,%d\n' % r
|
|
||||||
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
|
|
||||||
|
|
||||||
hw = Hostwatch()
|
def hostwatch_ready(sock):
|
||||||
hw.leftover = b('')
|
assert(hw.pid)
|
||||||
|
content = hw.sock.recv(4096)
|
||||||
def hostwatch_ready(sock):
|
if content:
|
||||||
assert(hw.pid)
|
lines = (hw.leftover + content).split(b('\n'))
|
||||||
content = hw.sock.recv(4096)
|
if lines[-1]:
|
||||||
if content:
|
# no terminating newline: entry isn't complete yet!
|
||||||
lines = (hw.leftover + content).split(b('\n'))
|
hw.leftover = lines.pop()
|
||||||
if lines[-1]:
|
lines.append(b(''))
|
||||||
# no terminating newline: entry isn't complete yet!
|
else:
|
||||||
hw.leftover = lines.pop()
|
hw.leftover = b('')
|
||||||
lines.append(b(''))
|
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
|
||||||
else:
|
else:
|
||||||
hw.leftover = b('')
|
raise Fatal('hostwatch process died')
|
||||||
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
|
|
||||||
else:
|
|
||||||
raise Fatal(' s: hostwatch process died')
|
|
||||||
|
|
||||||
def got_host_req(data):
|
def got_host_req(data):
|
||||||
if not hw.pid:
|
if not hw.pid:
|
||||||
(hw.pid, hw.sock) = start_hostwatch(
|
(hw.pid, hw.sock) = start_hostwatch(
|
||||||
data.decode("ASCII").strip().split(), auto_hosts)
|
data.decode("ASCII").strip().split(), auto_hosts)
|
||||||
handlers.append(Handler(socks=[hw.sock],
|
handlers.append(Handler(socks=[hw.sock],
|
||||||
callback=hostwatch_ready))
|
callback=hostwatch_ready))
|
||||||
mux.got_host_req = got_host_req
|
mux.got_host_req = got_host_req
|
||||||
|
|
||||||
def new_channel(channel, data):
|
def new_channel(channel, data):
|
||||||
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
|
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
|
||||||
family = int(family)
|
family = int(family)
|
||||||
# AF_INET is the same constant on Linux and BSD but AF_INET6
|
# AF_INET is the same constant on Linux and BSD but AF_INET6
|
||||||
# is different. As the client and server can be running on
|
# is different. As the client and server can be running on
|
||||||
# different platforms we can not just set the socket family
|
# different platforms we can not just set the socket family
|
||||||
# to what comes in the wire.
|
# to what comes in the wire.
|
||||||
if family != socket.AF_INET:
|
if family != socket.AF_INET:
|
||||||
family = socket.AF_INET6
|
family = socket.AF_INET6
|
||||||
dstport = int(dstport)
|
|
||||||
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
|
||||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
|
||||||
mux.new_channel = new_channel
|
|
||||||
|
|
||||||
dnshandlers = {}
|
|
||||||
|
|
||||||
def dns_req(channel, data):
|
|
||||||
debug2('Incoming DNS request channel=%d.\n' % channel)
|
|
||||||
h = DnsProxy(mux, channel, data, to_nameserver)
|
|
||||||
handlers.append(h)
|
|
||||||
dnshandlers[channel] = h
|
|
||||||
mux.got_dns_req = dns_req
|
|
||||||
|
|
||||||
udphandlers = {}
|
|
||||||
|
|
||||||
def udp_req(channel, cmd, data):
|
|
||||||
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
|
|
||||||
if cmd == ssnet.CMD_UDP_DATA:
|
|
||||||
(dstip, dstport, data) = data.split(b(','), 2)
|
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
|
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
||||||
h = udphandlers[channel]
|
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||||
h.send((dstip, dstport), data)
|
mux.new_channel = new_channel
|
||||||
elif cmd == ssnet.CMD_UDP_CLOSE:
|
|
||||||
debug2('is incoming UDP close\n')
|
|
||||||
h = udphandlers[channel]
|
|
||||||
h.ok = False
|
|
||||||
del mux.channels[channel]
|
|
||||||
|
|
||||||
def udp_open(channel, data):
|
dnshandlers = {}
|
||||||
debug2('Incoming UDP open.\n')
|
|
||||||
family = int(data)
|
def dns_req(channel, data):
|
||||||
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
|
debug2('Incoming DNS request channel=%d.' % channel)
|
||||||
if channel in udphandlers:
|
h = DnsProxy(mux, channel, data, to_nameserver)
|
||||||
raise Fatal(' s: UDP connection channel %d already open' % channel)
|
|
||||||
else:
|
|
||||||
h = UdpProxy(mux, channel, family)
|
|
||||||
handlers.append(h)
|
handlers.append(h)
|
||||||
udphandlers[channel] = h
|
dnshandlers[channel] = h
|
||||||
mux.got_udp_open = udp_open
|
mux.got_dns_req = dns_req
|
||||||
|
|
||||||
while mux.ok:
|
udphandlers = {}
|
||||||
if hw.pid:
|
|
||||||
assert(hw.pid > 0)
|
|
||||||
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
|
||||||
if rpid:
|
|
||||||
raise Fatal(
|
|
||||||
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
|
|
||||||
|
|
||||||
ssnet.runonce(handlers, mux)
|
def udp_req(channel, cmd, data):
|
||||||
if latency_control:
|
debug2('Incoming UDP request channel=%d, cmd=%d' %
|
||||||
mux.check_fullness()
|
(channel, cmd))
|
||||||
|
if cmd == ssnet.CMD_UDP_DATA:
|
||||||
|
(dstip, dstport, data) = data.split(b(','), 2)
|
||||||
|
dstport = int(dstport)
|
||||||
|
debug2('is incoming UDP data. %r %d.' % (dstip, dstport))
|
||||||
|
h = udphandlers[channel]
|
||||||
|
h.send((dstip, dstport), data)
|
||||||
|
elif cmd == ssnet.CMD_UDP_CLOSE:
|
||||||
|
debug2('is incoming UDP close')
|
||||||
|
h = udphandlers[channel]
|
||||||
|
h.ok = False
|
||||||
|
del mux.channels[channel]
|
||||||
|
|
||||||
if dnshandlers:
|
def udp_open(channel, data):
|
||||||
now = time.time()
|
debug2('Incoming UDP open.')
|
||||||
remove = []
|
family = int(data)
|
||||||
for channel, h in dnshandlers.items():
|
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd,
|
||||||
if h.timeout < now or not h.ok:
|
data)
|
||||||
debug3('expiring dnsreqs channel=%d\n' % channel)
|
if channel in udphandlers:
|
||||||
remove.append(channel)
|
raise Fatal('UDP connection channel %d already open' %
|
||||||
h.ok = False
|
channel)
|
||||||
for channel in remove:
|
else:
|
||||||
del dnshandlers[channel]
|
h = UdpProxy(mux, channel, family)
|
||||||
if udphandlers:
|
handlers.append(h)
|
||||||
remove = []
|
udphandlers[channel] = h
|
||||||
for channel, h in udphandlers.items():
|
mux.got_udp_open = udp_open
|
||||||
if not h.ok:
|
|
||||||
debug3('expiring UDP channel=%d\n' % channel)
|
while mux.ok:
|
||||||
remove.append(channel)
|
if hw.pid:
|
||||||
h.ok = False
|
assert(hw.pid > 0)
|
||||||
for channel in remove:
|
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
||||||
del udphandlers[channel]
|
if rpid:
|
||||||
|
raise Fatal(
|
||||||
|
'hostwatch exited unexpectedly: code 0x%04x' % rv)
|
||||||
|
|
||||||
|
ssnet.runonce(handlers, mux)
|
||||||
|
if latency_control:
|
||||||
|
mux.check_fullness()
|
||||||
|
|
||||||
|
if dnshandlers:
|
||||||
|
now = time.time()
|
||||||
|
remove = []
|
||||||
|
for channel, h in dnshandlers.items():
|
||||||
|
if h.timeout < now or not h.ok:
|
||||||
|
debug3('expiring dnsreqs channel=%d' % channel)
|
||||||
|
remove.append(channel)
|
||||||
|
h.ok = False
|
||||||
|
for channel in remove:
|
||||||
|
del dnshandlers[channel]
|
||||||
|
if udphandlers:
|
||||||
|
remove = []
|
||||||
|
for channel, h in udphandlers.items():
|
||||||
|
if not h.ok:
|
||||||
|
debug3('expiring UDP channel=%d' % channel)
|
||||||
|
remove.append(channel)
|
||||||
|
h.ok = False
|
||||||
|
for channel in remove:
|
||||||
|
del udphandlers[channel]
|
||||||
|
|
||||||
|
except Fatal as e:
|
||||||
|
log('fatal: %s' % e)
|
||||||
|
sys.exit(99)
|
||||||
|
@ -61,11 +61,11 @@ def parse_hostport(rhostport):
|
|||||||
if ":" in host:
|
if ":" in host:
|
||||||
# IPv6 address and/or got a port specified
|
# IPv6 address and/or got a port specified
|
||||||
|
|
||||||
# If it is an IPv6 adress with port specification,
|
# If it is an IPv6 address with port specification,
|
||||||
# then it will look like: [::1]:22
|
# then it will look like: [::1]:22
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# try to parse host as an IP adress,
|
# try to parse host as an IP address,
|
||||||
# if that works it is an IPv6 address
|
# if that works it is an IPv6 address
|
||||||
host = str(ipaddress.ip_address(host))
|
host = str(ipaddress.ip_address(host))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -103,11 +103,21 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
empackage(z, 'sshuttle.server') +
|
empackage(z, 'sshuttle.server') +
|
||||||
b"\n")
|
b"\n")
|
||||||
|
|
||||||
|
# If the exec() program calls sys.exit(), it should exit python
|
||||||
|
# and the sys.exit(98) call won't be reached (so we try to only
|
||||||
|
# exit that way in the server). However, if the code that we
|
||||||
|
# exec() simply returns from main, then we will return from
|
||||||
|
# exec(). If the server's python process dies, it should stop
|
||||||
|
# executing and also won't reach sys.exit(98).
|
||||||
|
#
|
||||||
|
# So, we shouldn't reach sys.exit(98) and we certainly shouldn't
|
||||||
|
# reach it immediately after trying to start the server.
|
||||||
pyscript = r"""
|
pyscript = r"""
|
||||||
import sys, os;
|
import sys, os;
|
||||||
verbosity=%d;
|
verbosity=%d;
|
||||||
sys.stdin = os.fdopen(0, "rb");
|
sys.stdin = os.fdopen(0, "rb");
|
||||||
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
|
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"));
|
||||||
|
sys.exit(98);
|
||||||
""" % (helpers.verbose or 0, len(content))
|
""" % (helpers.verbose or 0, len(content))
|
||||||
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
||||||
|
|
||||||
@ -127,8 +137,47 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
if python:
|
if python:
|
||||||
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
||||||
else:
|
else:
|
||||||
|
# By default, we run the following code in a shell.
|
||||||
|
# However, with restricted shells and other unusual
|
||||||
|
# situations, there can be trouble. See the RESTRICTED
|
||||||
|
# SHELL section in "man bash" for more information. The
|
||||||
|
# code makes many assumptions:
|
||||||
|
#
|
||||||
|
# (1) That /bin/sh exists and that we can call it.
|
||||||
|
# Restricted shells often do *not* allow you to run
|
||||||
|
# programs specified with an absolute path like /bin/sh.
|
||||||
|
# Either way, if there is trouble with this, it should
|
||||||
|
# return error code 127.
|
||||||
|
#
|
||||||
|
# (2) python3 or python exists in the PATH and is
|
||||||
|
# executable. If they aren't, then exec won't work (see (4)
|
||||||
|
# below).
|
||||||
|
#
|
||||||
|
# (3) In /bin/sh, that we can redirect stderr in order to
|
||||||
|
# hide the version that "python3 -V" might print (some
|
||||||
|
# restricted shells don't allow redirection, see
|
||||||
|
# RESTRICTED SHELL section in 'man bash'). However, if we
|
||||||
|
# are in a restricted shell, we'd likely have trouble with
|
||||||
|
# assumption (1) above.
|
||||||
|
#
|
||||||
|
# (4) The 'exec' command should work except if we failed
|
||||||
|
# to exec python because it doesn't exist or isn't
|
||||||
|
# executable OR if exec isn't allowed (some restricted
|
||||||
|
# shells don't allow exec). If the exec succeeded, it will
|
||||||
|
# not return and not get to the "exit 97" command. If exec
|
||||||
|
# does return, we exit with code 97.
|
||||||
|
#
|
||||||
|
# Specifying the exact python program to run with --python
|
||||||
|
# avoids many of the issues above. However, if
|
||||||
|
# you have a restricted shell on remote, you may only be
|
||||||
|
# able to run python if it is in your PATH (and you can't
|
||||||
|
# run programs specified with an absolute path). In that
|
||||||
|
# case, sshuttle might not work at all since it is not
|
||||||
|
# possible to run python on the remote machine---even if
|
||||||
|
# it is present.
|
||||||
pycmd = ("P=python3; $P -V 2>%s || P=python; "
|
pycmd = ("P=python3; $P -V 2>%s || P=python; "
|
||||||
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript))
|
"exec \"$P\" -c %s; exit 97") % \
|
||||||
|
(os.devnull, quote(pyscript))
|
||||||
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
|
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
|
||||||
|
|
||||||
if password is not None:
|
if password is not None:
|
||||||
@ -160,7 +209,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
|
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
|
||||||
s1.close()
|
s1.close()
|
||||||
|
|
||||||
debug2('executing: %r\n' % argv)
|
debug2('executing: %r' % argv)
|
||||||
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
||||||
close_fds=True, stderr=stderr)
|
close_fds=True, stderr=stderr)
|
||||||
os.close(s1a)
|
os.close(s1a)
|
||||||
|
@ -83,7 +83,7 @@ def _nb_clean(func, *args):
|
|||||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
debug3('%s: err was: %s\n' % (func.__name__, e))
|
debug3('%s: err was: %s' % (func.__name__, e))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ class SockWrapper:
|
|||||||
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
||||||
global _swcount
|
global _swcount
|
||||||
_swcount += 1
|
_swcount += 1
|
||||||
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
|
debug3('creating new SockWrapper (%d now exist)' % _swcount)
|
||||||
self.exc = None
|
self.exc = None
|
||||||
self.rsock = rsock
|
self.rsock = rsock
|
||||||
self.wsock = wsock
|
self.wsock = wsock
|
||||||
@ -124,9 +124,9 @@ class SockWrapper:
|
|||||||
def __del__(self):
|
def __del__(self):
|
||||||
global _swcount
|
global _swcount
|
||||||
_swcount -= 1
|
_swcount -= 1
|
||||||
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
|
debug1('%r: deleting (%d remain)' % (self, _swcount))
|
||||||
if self.exc:
|
if self.exc:
|
||||||
debug1('%r: error was: %s\n' % (self, self.exc))
|
debug1('%r: error was: %s' % (self, self.exc))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.rsock == self.wsock:
|
if self.rsock == self.wsock:
|
||||||
@ -148,14 +148,14 @@ class SockWrapper:
|
|||||||
if not self.connect_to:
|
if not self.connect_to:
|
||||||
return # already connected
|
return # already connected
|
||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
|
debug3('%r: trying connect to %r' % (self, self.connect_to))
|
||||||
try:
|
try:
|
||||||
self.rsock.connect(self.connect_to)
|
self.rsock.connect(self.connect_to)
|
||||||
# connected successfully (Linux)
|
# connected successfully (Linux)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
debug3('%r: connect result: %s\n' % (self, e))
|
debug3('%r: connect result: %s' % (self, e))
|
||||||
if e.args[0] == errno.EINVAL:
|
if e.args[0] == errno.EINVAL:
|
||||||
# this is what happens when you call connect() on a socket
|
# this is what happens when you call connect() on a socket
|
||||||
# that is now connected but returned EINPROGRESS last time,
|
# that is now connected but returned EINPROGRESS last time,
|
||||||
@ -165,7 +165,7 @@ class SockWrapper:
|
|||||||
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
|
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
|
||||||
socket.SO_ERROR)
|
socket.SO_ERROR)
|
||||||
e = socket.error(realerr, os.strerror(realerr))
|
e = socket.error(realerr, os.strerror(realerr))
|
||||||
debug3('%r: fixed connect result: %s\n' % (self, e))
|
debug3('%r: fixed connect result: %s' % (self, e))
|
||||||
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
|
||||||
pass # not connected yet
|
pass # not connected yet
|
||||||
elif e.args[0] == 0:
|
elif e.args[0] == 0:
|
||||||
@ -191,13 +191,13 @@ class SockWrapper:
|
|||||||
|
|
||||||
def noread(self):
|
def noread(self):
|
||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
debug2('%r: done reading\n' % self)
|
debug2('%r: done reading' % self)
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
|
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
|
||||||
|
|
||||||
def nowrite(self):
|
def nowrite(self):
|
||||||
if not self.shut_write:
|
if not self.shut_write:
|
||||||
debug2('%r: done writing\n' % self)
|
debug2('%r: done writing' % self)
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
try:
|
try:
|
||||||
self.wsock.shutdown(SHUT_WR)
|
self.wsock.shutdown(SHUT_WR)
|
||||||
@ -218,7 +218,7 @@ class SockWrapper:
|
|||||||
except OSError:
|
except OSError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.EPIPE:
|
if e.errno == errno.EPIPE:
|
||||||
debug1('%r: uwrite: got EPIPE\n' % self)
|
debug1('%r: uwrite: got EPIPE' % self)
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
@ -275,12 +275,12 @@ class Handler:
|
|||||||
_add(r, i)
|
_add(r, i)
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
log('--no callback defined-- %r\n' % self)
|
log('--no callback defined-- %r' % self)
|
||||||
(r, _, _) = select.select(self.socks, [], [], 0)
|
(r, _, _) = select.select(self.socks, [], [], 0)
|
||||||
for s in r:
|
for s in r:
|
||||||
v = s.recv(4096)
|
v = s.recv(4096)
|
||||||
if not v:
|
if not v:
|
||||||
log('--closed-- %r\n' % self)
|
log('--closed-- %r' % self)
|
||||||
self.socks = []
|
self.socks = []
|
||||||
self.ok = False
|
self.ok = False
|
||||||
|
|
||||||
@ -377,7 +377,7 @@ class Mux(Handler):
|
|||||||
# for b in self.outbuf:
|
# for b in self.outbuf:
|
||||||
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
|
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
|
||||||
# ob.append(c)
|
# ob.append(c)
|
||||||
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
# log('outbuf: %d %r' % (self.amount_queued(), ob))
|
||||||
|
|
||||||
def send(self, channel, cmd, data):
|
def send(self, channel, cmd, data):
|
||||||
assert isinstance(data, bytes)
|
assert isinstance(data, bytes)
|
||||||
@ -385,18 +385,18 @@ class Mux(Handler):
|
|||||||
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
|
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
|
||||||
+ data
|
+ data
|
||||||
self.outbuf.append(p)
|
self.outbuf.append(p)
|
||||||
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
||||||
len(data), self.fullness))
|
len(data), self.fullness))
|
||||||
self.fullness += len(data)
|
self.fullness += len(data)
|
||||||
|
|
||||||
def got_packet(self, channel, cmd, data):
|
def got_packet(self, channel, cmd, data):
|
||||||
debug2('< channel=%d cmd=%s len=%d\n'
|
debug2('< channel=%d cmd=%s len=%d'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
||||||
if cmd == CMD_PING:
|
if cmd == CMD_PING:
|
||||||
self.send(0, CMD_PONG, data)
|
self.send(0, CMD_PONG, data)
|
||||||
elif cmd == CMD_PONG:
|
elif cmd == CMD_PONG:
|
||||||
debug2('received PING response\n')
|
debug2('received PING response')
|
||||||
self.too_full = False
|
self.too_full = False
|
||||||
self.fullness = 0
|
self.fullness = 0
|
||||||
elif cmd == CMD_EXIT:
|
elif cmd == CMD_EXIT:
|
||||||
@ -431,7 +431,7 @@ class Mux(Handler):
|
|||||||
else:
|
else:
|
||||||
callback = self.channels.get(channel)
|
callback = self.channels.get(channel)
|
||||||
if not callback:
|
if not callback:
|
||||||
log('warning: closed channel %d got cmd=%s len=%d\n'
|
log('warning: closed channel %d got cmd=%s len=%d'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
|
||||||
else:
|
else:
|
||||||
callback(cmd, data)
|
callback(cmd, data)
|
||||||
@ -446,7 +446,7 @@ class Mux(Handler):
|
|||||||
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
|
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
|
||||||
if self.outbuf and self.outbuf[0]:
|
if self.outbuf and self.outbuf[0]:
|
||||||
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
|
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
|
||||||
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
|
debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0])))
|
||||||
if wrote:
|
if wrote:
|
||||||
self.outbuf[0] = self.outbuf[0][wrote:]
|
self.outbuf[0] = self.outbuf[0][wrote:]
|
||||||
while self.outbuf and not self.outbuf[0]:
|
while self.outbuf and not self.outbuf[0]:
|
||||||
@ -461,11 +461,14 @@ class Mux(Handler):
|
|||||||
flags |= os.O_NONBLOCK
|
flags |= os.O_NONBLOCK
|
||||||
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
|
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
|
||||||
try:
|
try:
|
||||||
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
|
# If LATENCY_BUFFER_SIZE is inappropriately large, we will
|
||||||
|
# get a MemoryError here. Read no more than 1MiB.
|
||||||
|
read = _nb_clean(os.read, self.rfile.fileno(),
|
||||||
|
min(1048576, LATENCY_BUFFER_SIZE))
|
||||||
except OSError:
|
except OSError:
|
||||||
_, e = sys.exc_info()[:2]
|
_, e = sys.exc_info()[:2]
|
||||||
raise Fatal('other end: %r' % e)
|
raise Fatal('other end: %r' % e)
|
||||||
# log('<<< %r\n' % b)
|
# log('<<< %r' % b)
|
||||||
if read == b(''): # EOF
|
if read == b(''): # EOF
|
||||||
self.ok = False
|
self.ok = False
|
||||||
if read:
|
if read:
|
||||||
@ -473,7 +476,7 @@ class Mux(Handler):
|
|||||||
|
|
||||||
def handle(self):
|
def handle(self):
|
||||||
self.fill()
|
self.fill()
|
||||||
# log('inbuf is: (%d,%d) %r\n'
|
# log('inbuf is: (%d,%d) %r'
|
||||||
# % (self.want, len(self.inbuf), self.inbuf))
|
# % (self.want, len(self.inbuf), self.inbuf))
|
||||||
while 1:
|
while 1:
|
||||||
if len(self.inbuf) >= (self.want or HDR_LEN):
|
if len(self.inbuf) >= (self.want or HDR_LEN):
|
||||||
@ -511,7 +514,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.mux.channels[channel] = self.got_packet
|
self.mux.channels[channel] = self.got_packet
|
||||||
self.socks = []
|
self.socks = []
|
||||||
debug2('new channel: %d\n' % channel)
|
debug2('new channel: %d' % channel)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
@ -527,7 +530,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def setnoread(self):
|
def setnoread(self):
|
||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
debug2('%r: done reading\n' % self)
|
debug2('%r: done reading' % self)
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
@ -538,13 +541,13 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def setnowrite(self):
|
def setnowrite(self):
|
||||||
if not self.shut_write:
|
if not self.shut_write:
|
||||||
debug2('%r: done writing\n' % self)
|
debug2('%r: done writing' % self)
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
def maybe_close(self):
|
def maybe_close(self):
|
||||||
if self.shut_read and self.shut_write:
|
if self.shut_read and self.shut_write:
|
||||||
debug2('%r: closing connection\n' % self)
|
debug2('%r: closing connection' % self)
|
||||||
# remove the mux's reference to us. The python garbage collector
|
# remove the mux's reference to us. The python garbage collector
|
||||||
# will then be able to reap our object.
|
# will then be able to reap our object.
|
||||||
self.mux.channels[self.channel] = None
|
self.mux.channels[self.channel] = None
|
||||||
@ -581,9 +584,9 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
|
|
||||||
def connect_dst(family, ip, port):
|
def connect_dst(family, ip, port):
|
||||||
debug2('Connecting to %s:%d\n' % (ip, port))
|
debug2('Connecting to %s:%d' % (ip, port))
|
||||||
outsock = socket.socket(family)
|
outsock = socket.socket(family)
|
||||||
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
|
|
||||||
return SockWrapper(outsock, outsock,
|
return SockWrapper(outsock, outsock,
|
||||||
connect_to=(ip, port),
|
connect_to=(ip, port),
|
||||||
peername='%s:%d' % (ip, port))
|
peername='%s:%d' % (ip, port))
|
||||||
@ -599,11 +602,11 @@ def runonce(handlers, mux):
|
|||||||
|
|
||||||
for s in handlers:
|
for s in handlers:
|
||||||
s.pre_select(r, w, x)
|
s.pre_select(r, w, x)
|
||||||
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
|
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)'
|
||||||
% (len(handlers), _fds(r), _fds(w), _fds(x),
|
% (len(handlers), _fds(r), _fds(w), _fds(x),
|
||||||
mux.fullness, mux.too_full))
|
mux.fullness, mux.too_full))
|
||||||
(r, w, x) = select.select(r, w, x)
|
(r, w, x) = select.select(r, w, x)
|
||||||
debug2(' Ready: %d r=%r w=%r x=%r\n'
|
debug2(' Ready: %d r=%r w=%r x=%r'
|
||||||
% (len(handlers), _fds(r), _fds(w), _fds(x)))
|
% (len(handlers), _fds(r), _fds(w), _fds(x)))
|
||||||
ready = r + w + x
|
ready = r + w + x
|
||||||
did = {}
|
did = {}
|
||||||
|
@ -19,9 +19,14 @@ Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
|
|||||||
%(user_name)s ALL=NOPASSWD: %(ca)s
|
%(user_name)s ALL=NOPASSWD: %(ca)s
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
warning_msg = "# WARNING: When you allow a user to run sshuttle as root,\n" \
|
||||||
|
"# they can then use sshuttle's --ssh-cmd option to run any\n" \
|
||||||
|
"# command as root.\n"
|
||||||
|
|
||||||
|
|
||||||
def build_config(user_name):
|
def build_config(user_name):
|
||||||
content = template % {
|
content = warning_msg
|
||||||
|
content += template % {
|
||||||
'ca': command_alias,
|
'ca': command_alias,
|
||||||
'dist_packages': path_to_dist_packages,
|
'dist_packages': path_to_dist_packages,
|
||||||
'py': sys.executable,
|
'py': sys.executable,
|
||||||
@ -42,14 +47,15 @@ def save_config(content, file_name):
|
|||||||
process.stdin.write(content.encode())
|
process.stdin.write(content.encode())
|
||||||
|
|
||||||
streamdata = process.communicate()[0]
|
streamdata = process.communicate()[0]
|
||||||
|
sys.stdout.write(streamdata.decode("ASCII"))
|
||||||
returncode = process.returncode
|
returncode = process.returncode
|
||||||
|
|
||||||
if returncode:
|
if returncode:
|
||||||
log('Failed updating sudoers file.\n')
|
log('Failed updating sudoers file.')
|
||||||
debug1(streamdata)
|
debug1(streamdata)
|
||||||
exit(returncode)
|
exit(returncode)
|
||||||
else:
|
else:
|
||||||
log('Success, sudoers file update.\n')
|
log('Success, sudoers file update.')
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
@ -61,4 +67,5 @@ def sudoers(user_name=None, no_modify=None, file_name=None):
|
|||||||
sys.stdout.write(content)
|
sys.stdout.write(content)
|
||||||
exit(0)
|
exit(0)
|
||||||
else:
|
else:
|
||||||
|
sys.stdout.write(warning_msg)
|
||||||
save_config(content, file_name)
|
save_config(content, file_name)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import io
|
import io
|
||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
from mock import Mock, patch, call
|
from unittest.mock import Mock, patch, call
|
||||||
import sshuttle.firewall
|
import sshuttle.firewall
|
||||||
|
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ NSLIST
|
|||||||
{inet},1.2.3.33
|
{inet},1.2.3.33
|
||||||
{inet6},2404:6800:4004:80c::33
|
{inet6},2404:6800:4004:80c::33
|
||||||
PORTS 1024,1025,1026,1027
|
PORTS 1024,1025,1026,1027
|
||||||
GO 1 -
|
GO 1 - 0x01 12345
|
||||||
HOST 1.2.3.3,existing
|
HOST 1.2.3.3,existing
|
||||||
""".format(inet=AF_INET, inet6=AF_INET6))
|
""".format(inet=AF_INET, inet6=AF_INET6))
|
||||||
stdout = Mock()
|
stdout = Mock()
|
||||||
@ -125,7 +125,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None),
|
None,
|
||||||
|
'0x01'),
|
||||||
call().setup_firewall(
|
call().setup_firewall(
|
||||||
1025, 1027,
|
1025, 1027,
|
||||||
[(AF_INET, u'1.2.3.33')],
|
[(AF_INET, u'1.2.3.33')],
|
||||||
@ -133,7 +134,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
True,
|
True,
|
||||||
None),
|
None,
|
||||||
|
'0x01'),
|
||||||
call().restore_firewall(1024, AF_INET6, True, None),
|
call().restore_firewall(1024, AF_INET6, True, None),
|
||||||
call().restore_firewall(1025, AF_INET, True, None),
|
call().restore_firewall(1025, AF_INET, True, None),
|
||||||
]
|
]
|
||||||
|
@ -3,7 +3,7 @@ import socket
|
|||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
import errno
|
import errno
|
||||||
|
|
||||||
from mock import patch, call
|
from unittest.mock import patch, call
|
||||||
import sshuttle.helpers
|
import sshuttle.helpers
|
||||||
|
|
||||||
|
|
||||||
@ -24,19 +24,19 @@ def test_log(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message'),
|
call.write('prefix: message\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: abc'),
|
call.write('prefix: abc\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: message 1\n'),
|
call.write('prefix: message 1\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: message 2\n'),
|
call.write('prefix: message 2\r\n'),
|
||||||
call.write('---> line2\n'),
|
call.write(' line2\r\n'),
|
||||||
call.write('---> line3\n'),
|
call.write(' line3\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
call.write('prefix: message 3\n'),
|
call.write('prefix: message 3\r\n'),
|
||||||
call.write('---> line2\n'),
|
call.write(' line2\r\n'),
|
||||||
call.write('---> line3\n'),
|
call.write(' line3\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message'),
|
call.write('prefix: message\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message'),
|
call.write('prefix: message\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout):
|
|||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
assert mock_stderr.mock_calls == [
|
assert mock_stderr.mock_calls == [
|
||||||
call.write('prefix: message'),
|
call.write('prefix: message\r\n'),
|
||||||
call.flush(),
|
call.flush(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from socket import AF_INET, AF_INET6
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from mock import Mock, patch, call
|
from unittest.mock import Mock, patch, call
|
||||||
from sshuttle.helpers import Fatal
|
from sshuttle.helpers import Fatal
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ from sshuttle.methods import get_method
|
|||||||
def test_get_supported_features():
|
def test_get_supported_features():
|
||||||
method = get_method('nat')
|
method = get_method('nat')
|
||||||
features = method.get_supported_features()
|
features = method.get_supported_features()
|
||||||
assert not features.ipv6
|
assert features.ipv6
|
||||||
assert not features.udp
|
assert not features.udp
|
||||||
assert features.dns
|
assert features.dns
|
||||||
|
|
||||||
@ -85,26 +85,52 @@ def test_firewall_command():
|
|||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.nat.ipt')
|
@patch('sshuttle.methods.nat.ipt')
|
||||||
@patch('sshuttle.methods.nat.ipt_ttl')
|
|
||||||
@patch('sshuttle.methods.nat.ipt_chain_exists')
|
@patch('sshuttle.methods.nat.ipt_chain_exists')
|
||||||
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
||||||
mock_ipt_chain_exists.return_value = True
|
mock_ipt_chain_exists.return_value = True
|
||||||
method = get_method('nat')
|
method = get_method('nat')
|
||||||
assert method.name == 'nat'
|
assert method.name == 'nat'
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
|
||||||
method.setup_firewall(
|
|
||||||
1024, 1026,
|
|
||||||
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
|
||||||
AF_INET6,
|
|
||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
|
||||||
True,
|
|
||||||
None)
|
|
||||||
assert str(excinfo.value) \
|
|
||||||
== 'Address family "AF_INET6" unsupported by nat method_name'
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == []
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
assert mock_ipt.mock_calls == []
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
|
AF_INET6,
|
||||||
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
||||||
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
'0x01')
|
||||||
|
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(AF_INET6, 'nat', 'sshuttle-1024')
|
||||||
|
]
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-F', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-X', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-N', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-F', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1024'),
|
||||||
|
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT',
|
||||||
|
'--dest', u'2404:6800:4004:80c::33', '-p', 'udp',
|
||||||
|
'--dport', '53', '--to-ports', '1026'),
|
||||||
|
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
||||||
|
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128', '-p', 'tcp',
|
||||||
|
'--dport', '80:80'),
|
||||||
|
call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT',
|
||||||
|
'--dest', u'2404:6800:4004:80c::/64', '-p', 'tcp',
|
||||||
|
'--to-ports', '1024')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == []
|
assert mock_ipt.mock_calls == []
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
with pytest.raises(Exception) as excinfo:
|
||||||
@ -115,10 +141,10 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
True,
|
True,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert str(excinfo.value) == 'UDP not supported by nat method_name'
|
assert str(excinfo.value) == 'UDP not supported by nat method_name'
|
||||||
assert mock_ipt_chain_exists.mock_calls == []
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == []
|
assert mock_ipt.mock_calls == []
|
||||||
|
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
@ -128,14 +154,11 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET, 'nat', 'sshuttle-1025')
|
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == [
|
|
||||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
|
||||||
'-m', 'ttl', '--ttl', '63')
|
|
||||||
]
|
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||||
@ -146,7 +169,7 @@ 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', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||||
'--dest', u'1.2.3.33/32', '-p', 'udp',
|
'--dest', u'1.2.3.33', '-p', 'udp',
|
||||||
'--dport', '53', '--to-ports', '1027'),
|
'--dport', '53', '--to-ports', '1027'),
|
||||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
||||||
@ -157,20 +180,33 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
'--to-ports', '1025')
|
'--to-ports', '1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET, False, None)
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET, 'nat', 'sshuttle-1025')
|
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j',
|
||||||
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
'sshuttle-1025'),
|
||||||
|
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j',
|
||||||
|
'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
||||||
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
|
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
|
method.restore_firewall(1025, AF_INET6, False, None)
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(AF_INET6, 'nat', 'sshuttle-1025')
|
||||||
|
]
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
|
call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j',
|
||||||
|
'sshuttle-1025'),
|
||||||
|
call(AF_INET6, 'nat', '-F', 'sshuttle-1025'),
|
||||||
|
call(AF_INET6, 'nat', '-X', 'sshuttle-1025')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
@ -2,7 +2,7 @@ import socket
|
|||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from mock import Mock, patch, call, ANY
|
from unittest.mock import Mock, patch, call, ANY
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
from sshuttle.helpers import Fatal, get_env
|
from sshuttle.helpers import Fatal, get_env
|
||||||
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
|
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
|
||||||
@ -186,7 +186,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
@ -225,7 +226,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
@ -238,7 +240,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
False,
|
False,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
@ -298,7 +301,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
|
|
||||||
assert mock_pfctl.mock_calls == [
|
assert mock_pfctl.mock_calls == [
|
||||||
call('-s all'),
|
call('-s all'),
|
||||||
@ -330,7 +334,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
@ -343,7 +348,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
False,
|
False,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
||||||
@ -401,7 +407,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
False,
|
False,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
|
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||||
@ -437,7 +444,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
assert mock_ioctl.mock_calls == []
|
assert mock_ioctl.mock_calls == []
|
||||||
@ -450,7 +458,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
False,
|
False,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert mock_ioctl.mock_calls == [
|
assert mock_ioctl.mock_calls == [
|
||||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import socket
|
import socket
|
||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
from mock import Mock, patch, call
|
from unittest.mock import Mock, patch, call
|
||||||
|
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
|
|
||||||
@patch("sshuttle.methods.tproxy.recvmsg")
|
def test_get_supported_features():
|
||||||
def test_get_supported_features_recvmsg(mock_recvmsg):
|
|
||||||
method = get_method('tproxy')
|
method = get_method('tproxy')
|
||||||
features = method.get_supported_features()
|
features = method.get_supported_features()
|
||||||
assert features.ipv6
|
assert features.ipv6
|
||||||
@ -15,15 +14,6 @@ def test_get_supported_features_recvmsg(mock_recvmsg):
|
|||||||
assert features.dns
|
assert features.dns
|
||||||
|
|
||||||
|
|
||||||
@patch("sshuttle.methods.tproxy.recvmsg", None)
|
|
||||||
def test_get_supported_features_norecvmsg():
|
|
||||||
method = get_method('tproxy')
|
|
||||||
features = method.get_supported_features()
|
|
||||||
assert features.ipv6
|
|
||||||
assert not features.udp
|
|
||||||
assert not features.dns
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_tcp_dstip():
|
def test_get_tcp_dstip():
|
||||||
sock = Mock()
|
sock = Mock()
|
||||||
sock.getsockname.return_value = ('127.0.0.1', 1024)
|
sock.getsockname.return_value = ('127.0.0.1', 1024)
|
||||||
@ -92,9 +82,8 @@ def test_firewall_command():
|
|||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.tproxy.ipt')
|
@patch('sshuttle.methods.tproxy.ipt')
|
||||||
@patch('sshuttle.methods.tproxy.ipt_ttl')
|
|
||||||
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
|
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
|
||||||
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt):
|
||||||
mock_ipt_chain_exists.return_value = True
|
mock_ipt_chain_exists.return_value = True
|
||||||
method = get_method('tproxy')
|
method = get_method('tproxy')
|
||||||
assert method.name == 'tproxy'
|
assert method.name == 'tproxy'
|
||||||
@ -108,13 +97,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
|
||||||
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||||
True,
|
True,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
|
call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-d-1024')
|
call(AF_INET6, 'mangle', 'sshuttle-d-1024')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
|
||||||
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
|
||||||
@ -133,18 +122,22 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
||||||
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||||
'sshuttle-t-1024'),
|
'sshuttle-t-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
|
||||||
'--set-mark', '1'),
|
'--set-mark', '0x01'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
|
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
|
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
|
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1',
|
'--tproxy-mark', '0x01',
|
||||||
'--dest', u'2404:6800:4004:80c::33/32',
|
'--dest', u'2404:6800:4004:80c::33/32',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
|
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
||||||
@ -160,22 +153,23 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
'--dest', u'2404:6800:4004:80c::101f/128',
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
|
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
|
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
'--tproxy-mark', '0x01', '--dest',
|
||||||
|
u'2404:6800:4004:80c::/64',
|
||||||
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
|
'-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
|
||||||
'--on-port', '1024'),
|
'--on-port', '1024'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
|
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
|
||||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
'--tproxy-mark', '0x01', '--dest',
|
||||||
|
u'2404:6800:4004:80c::/64',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
|
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
|
||||||
'--on-port', '1024')
|
'--on-port', '1024')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET6, True, None)
|
method.restore_firewall(1025, AF_INET6, True, None)
|
||||||
@ -184,7 +178,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
call(AF_INET6, 'mangle', 'sshuttle-t-1025'),
|
call(AF_INET6, 'mangle', 'sshuttle-t-1025'),
|
||||||
call(AF_INET6, 'mangle', 'sshuttle-d-1025')
|
call(AF_INET6, 'mangle', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'),
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
@ -196,7 +189,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
# IPV4
|
# IPV4
|
||||||
@ -208,13 +200,13 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
True,
|
True,
|
||||||
None)
|
None,
|
||||||
|
'0x01')
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
||||||
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
@ -233,18 +225,22 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||||
'sshuttle-t-1025'),
|
'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
||||||
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
|
||||||
'-j', 'MARK', '--set-mark', '1'),
|
'-j', 'MARK', '--set-mark', '0x01'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
|
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
|
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
||||||
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
|
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
'--set-mark', '1', '--dest', u'1.2.3.33/32',
|
'--set-mark', '0x01', '--dest', u'1.2.3.33/32',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
|
'--tproxy-mark', '0x01', '--dest', u'1.2.3.33/32',
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
|
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
||||||
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
|
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
|
||||||
@ -259,20 +255,19 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
|
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
|
||||||
'--dport', '80:80'),
|
'--dport', '80:80'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
'--set-mark', '0x01', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'tcp', '-p', 'tcp'),
|
'-m', 'tcp', '-p', 'tcp'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
|
'--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
|
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
'--set-mark', '0x01', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'udp', '-p', 'udp'),
|
'-m', 'udp', '-p', 'udp'),
|
||||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
|
'--tproxy-mark', '0x01', '--dest', u'1.2.3.0/24',
|
||||||
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
|
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, AF_INET, True, None)
|
method.restore_firewall(1025, AF_INET, True, None)
|
||||||
@ -281,7 +276,6 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
||||||
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
@ -293,5 +287,4 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
|
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
@ -100,3 +100,8 @@ def test_parse_subnetport_ip6_with_mask_and_port():
|
|||||||
== [(socket.AF_INET6, ip, 128, 80, 80)]
|
== [(socket.AF_INET6, ip, 128, 80, 80)]
|
||||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
|
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
|
||||||
== [(socket.AF_INET6, ip, 16, 80, 90)]
|
== [(socket.AF_INET6, ip, 16, 80, 90)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_arg_line_to_args_skips_comments():
|
||||||
|
parser = sshuttle.options.MyArgumentParser()
|
||||||
|
assert parser.convert_arg_line_to_args("# whatever something") == []
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import socket
|
import socket
|
||||||
|
|
||||||
from mock import Mock, patch, call
|
from unittest.mock import Mock, patch, call
|
||||||
|
|
||||||
import sshuttle.sdnotify
|
import sshuttle.sdnotify
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import io
|
import io
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from mock import patch, Mock
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
import sshuttle.server
|
import sshuttle.server
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user