mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 16:50:34 +02:00
Compare commits
375 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 | |||
bc72bb4811 | |||
b8cd2fae40 | |||
8f0d3b0f8e | |||
6d4261e3f9 | |||
7c338866bf | |||
6b87ad3fc7 | |||
0efd23f3b8 | |||
9bdd9fea5d | |||
d5cceb3e42 | |||
65b139ff6e | |||
76b8b83e22 | |||
a5214e0fd7 | |||
3861d29de2 | |||
59a983f9a6 | |||
4a65f97c8b | |||
461e676973 | |||
70e3e017ab | |||
26704cf742 | |||
28a85928be | |||
ff70f584d2 | |||
5ff834bd32 | |||
6b75d62d68 | |||
6bbe8c0d34 | |||
7e726bc235 | |||
227412e218 | |||
9b036fc689 | |||
34acdd0611 | |||
8461e08bc3 | |||
d3700f09da | |||
92b99442c3 | |||
709e5d1595 | |||
b5aaeda2a8 | |||
0ce268f21b | |||
34f538ff98 | |||
7d89b2d89f | |||
502960d796 | |||
5c8c707208 | |||
68c9c9bbcd | |||
c02b93e719 | |||
6d86e44fb4 | |||
ebf87d8f3b | |||
bc24ed359a | |||
ac3ccb769a | |||
1f3c74a1af | |||
512a3a8d96 | |||
4deee45bc6 | |||
7cb30b783d | |||
fb4950fafc | |||
c3016f2d90 | |||
9d704b3503 | |||
a266e7a8bd | |||
e1106a33a9 | |||
574ed8e564 | |||
1dbf216369 | |||
52558174b8 | |||
b7a29acab7 | |||
c2b10465e7 | |||
cfe14f2498 | |||
cb53d8a150 | |||
64d5c77a71 | |||
036c49e412 | |||
c1cc3911df | |||
84e43d3113 | |||
afad317f2c | |||
ae5dbd3b4d | |||
2995a624f1 | |||
909402a353 | |||
16148ac70f | |||
e50bbc2c92 | |||
9eaed73098 | |||
4b07dab9dc | |||
299854d2b5 | |||
8b71c150c6 | |||
dcce0fa3df | |||
3ee74ddfa6 | |||
7573011a08 | |||
72ae202df3 | |||
5d6b14673f | |||
aa97742405 | |||
19f653df36 | |||
ec5fb68350 | |||
d9e5ccc19c | |||
f23510a4fc | |||
459e573019 | |||
c12d2ba5c6 | |||
630f8c2357 | |||
e8f3b53c7d | |||
8ee230bca7 | |||
abb48f1996 | |||
1c27a6cad0 | |||
8a2d5802c1 | |||
e7d4931b3d | |||
1e364b2c0b | |||
8816dbfd23 | |||
98d052d19e | |||
be4b081a0d | |||
9c5f1f5bbf | |||
33d09ffcaf | |||
45f8cce2f8 | |||
d4001c11f9 | |||
450ad79b18 | |||
5debf1f11a | |||
79181043bc | |||
c0a81353ab | |||
5bdf36152a | |||
a9ee66d905 | |||
094d3d9b97 | |||
19b677892e | |||
319c122861 | |||
f4bd290919 | |||
f353701f24 | |||
3037a91e51 | |||
cdd1e2c538 | |||
eb01c0b184 | |||
c5dcc918db | |||
329b9cd0a0 | |||
5537a90338 | |||
636e0442e5 | |||
dc526747b1 | |||
73eb3b6479 | |||
1b50d364c6 | |||
8c91958ff3 | |||
d2f751f0d3 | |||
9d79bb82c5 | |||
a53f026056 | |||
ae4c7e3a7b | |||
61bbbca956 | |||
e56f8f2349 | |||
0a36eac686 | |||
16b462880b | |||
500aa65693 | |||
7d998f6d42 | |||
8c9dad1c6b | |||
7fde5b6fa6 | |||
734168531f | |||
d058d9bc93 | |||
1db3281c16 | |||
bef54e778d | |||
9bcca27965 | |||
d0f0aa9f17 | |||
ec2018d664 | |||
c9bc389992 | |||
9f27c1943b | |||
6c21addde9 | |||
4b320180c4 | |||
994ce07466 | |||
34197c492c | |||
75eaac7e06 | |||
b0c87b01b7 | |||
cf32a5cfa8 | |||
f674aacdc8 | |||
432f86f253 | |||
b63e58f494 | |||
88ce5c0bca | |||
50a4c36635 | |||
25461c70a3 | |||
365d30de14 | |||
6e19496fb7 | |||
534ad8dfed | |||
535eb62928 | |||
966fd0c523 | |||
580462156e | |||
9e78abd2c2 | |||
e3201969b5 | |||
9b10df21b6 | |||
accb4ecc97 | |||
ee36cc0d00 | |||
f0c13eafe9 | |||
36d34b18da | |||
3edeb726b8 | |||
6e9c58b4b4 | |||
13db89916a | |||
84076f29fa | |||
ad31ac4e18 | |||
69d3f7dc64 | |||
6ad4473c87 | |||
23516ebd71 | |||
c69b9d6f4b | |||
2d92090625 | |||
502b36e990 | |||
fe1df00be7 | |||
a32689d030 | |||
a7193f508a | |||
7ebff92637 | |||
138d2877c6 | |||
21ef365c59 | |||
a765aa3235 | |||
71f2248b07 | |||
935393b261 | |||
3e2ad68796 | |||
635cf8605e | |||
cb917d7e6c | |||
4372c6c117 | |||
4e945ca4de | |||
3bfb975ed9 | |||
752a953101 | |||
61f4cd9de5 | |||
8e35f049e2 | |||
0e99adc5d1 | |||
04849df7e3 | |||
531a17c151 | |||
d43db80dec | |||
0b1a260436 | |||
efc854c33e | |||
ca41026c89 | |||
b473b91633 | |||
7a54d12f80 | |||
d4bbf3b68d | |||
41f5b3e9c1 | |||
c780597de3 | |||
d085a419b2 | |||
842768f9cf | |||
97ed2030f3 | |||
6dc368bde8 | |||
f528bb9846 | |||
561b648e4b | |||
0dba8a8beb | |||
7b6f082454 | |||
1ec17e1b1b | |||
cecccc2efd | |||
db69ba6d8d | |||
2bb92cd6d4 | |||
ae5bd28dcf | |||
55bd78fd43 | |||
1f5ed9c66e |
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
|
37
.github/workflows/pythonpackage.yml
vendored
Normal file
37
.github/workflows/pythonpackage.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2.3.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-tests.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
flake8 sshuttle tests --count --show-source --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
PYTHONPATH=$PWD pytest
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,3 +15,4 @@
|
||||
/.redo
|
||||
/.pytest_cache/
|
||||
/.python-version
|
||||
.vscode/
|
||||
|
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: .
|
19
.travis.yml
19
.travis.yml
@ -1,19 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- pypy
|
||||
|
||||
install:
|
||||
- travis_retry pip install -q -r requirements-tests.txt
|
||||
|
||||
before_script:
|
||||
# stop the build if there are Python syntax errors or undefined names.
|
||||
- flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide.
|
||||
- flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
script:
|
||||
- PYTHONPATH=. py.test
|
155
CHANGES.rst
155
CHANGES.rst
@ -1,12 +1,157 @@
|
||||
==========
|
||||
Change log
|
||||
==========
|
||||
All notable changes to this project will be documented in this file. The format
|
||||
is based on `Keep a Changelog`_ and this project
|
||||
adheres to `Semantic Versioning`_.
|
||||
Release notes now moved to https://github.com/sshuttle/sshuttle/releases/
|
||||
|
||||
.. _`Keep a Changelog`: http://keepachangelog.com/
|
||||
.. _`Semantic Versioning`: http://semver.org/
|
||||
These are the old release notes.
|
||||
|
||||
|
||||
1.0.5 - 2020-12-29
|
||||
------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
* IPv6 support in nft method.
|
||||
* Intercept DNS requests sent by systemd-resolved.
|
||||
* Set default tmark.
|
||||
* Fix python2 server compatibility.
|
||||
* Python 3.9 support.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Change license text to LGPL-2.1
|
||||
* Fix #494 sshuttle caught in infinite select() loop.
|
||||
* Include sshuttle version in verbose output.
|
||||
* Add psutil as dependency in setup.py
|
||||
* When subnets and excludes are specified with hostnames, use all IPs.
|
||||
* Update/document client's handling of IPv4 and IPv6.
|
||||
* Update sdnotify.py documentation.
|
||||
* Allow no remote to work.
|
||||
* Make prefixes in verbose output more consistent.
|
||||
* Make nat and nft rules consistent; improve rule ordering.
|
||||
* Make server and client handle resolv.conf differently.
|
||||
* Fix handling OSError in FirewallClient#__init__
|
||||
* Refactor automatic method selection.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
* Drop testing of Python 3.5
|
||||
|
||||
|
||||
1.0.4 - 2020-08-24
|
||||
------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Allow Mux() flush/fill to work with python < 3.5
|
||||
* Fix parse_hostport to always return string for host.
|
||||
* Require -r/--remote parameter.
|
||||
* Add missing package in OpenWRT documentation.
|
||||
* Fix doc about --listen option.
|
||||
* README: add Ubuntu.
|
||||
* Increase IP4 ttl to 63 hops instead of 42.
|
||||
* Fix formatting in installation.rst
|
||||
|
||||
|
||||
1.0.3 - 2020-07-12
|
||||
------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Ask setuptools to require Python 3.5 and above.
|
||||
* Add missing import.
|
||||
* Fix formatting typos in usage docs
|
||||
|
||||
|
||||
1.0.2 - 2020-06-18
|
||||
------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Leave use of default port to ssh command.
|
||||
* Remove unwanted references to Python 2.7 in docs.
|
||||
* Replace usage of deprecated imp.
|
||||
* Fix connection with @ sign in username.
|
||||
|
||||
|
||||
1.0.1 - 2020-06-05
|
||||
------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Errors in python long_documentation.
|
||||
|
||||
|
||||
1.0.0 - 2020-06-05
|
||||
------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
* Python 3.8 support.
|
||||
* sshpass support.
|
||||
* Auto sudoers file (#269).
|
||||
* option for latency control buffer size.
|
||||
* Docs: FreeBSD'.
|
||||
* Docs: Nix'.
|
||||
* Docs: openwrt'.
|
||||
* Docs: install instructions for Fedora'.
|
||||
* Docs: install instructions for Arch Linux'.
|
||||
* Docs: 'My VPN broke and need a solution fast'.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
* Python 2.6 support.
|
||||
* Python 2.7 support.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Remove debug message for getpeername failure.
|
||||
* Fix crash triggered by port scans closing socket.
|
||||
* Added "Running as a service" to docs.
|
||||
* Systemd integration.
|
||||
* Trap UnicodeError to handle cases where hostnames returned by DNS are invalid.
|
||||
* Formatting error in CHANGES.rst
|
||||
* Various errors in documentation.
|
||||
* Nftables based method.
|
||||
* Make hostwatch locale-independent (#379).
|
||||
* Add tproxy udp port mark filter that was missed in #144, fixes #367.
|
||||
* Capturing of local DNS servers.
|
||||
* Crashing on ECONNABORTED.
|
||||
* Size of pf_rule, which grew in OpenBSD 6.4.
|
||||
* Use prompt for sudo, not needed for doas.
|
||||
* Arch linux installation instructions.
|
||||
* tests for existing PR-312 (#337).
|
||||
* Hyphen in hostname.
|
||||
* Assembler import (#319).
|
||||
|
||||
|
||||
0.78.5 - 2019-01-28
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
* doas support as replacement for sudo on OpenBSD.
|
||||
* Added ChromeOS section to documentation (#262)
|
||||
* Add --no-sudo-pythonpath option
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Fix forwarding to a single port.
|
||||
* Various updates to documentation.
|
||||
* Don't crash if we can't look up peername
|
||||
* Fix missing string formatting argument
|
||||
* Moved sshuttle/tests into tests.
|
||||
* Updated bandit config.
|
||||
* Replace path /dev/null by os.devnull.
|
||||
* Added coverage report to tests.
|
||||
* Fixes support for OpenBSD (6.1+) (#282).
|
||||
* Close stdin, stdout, and stderr when using syslog or forking to daemon (#283).
|
||||
* Changes pf exclusion rules precedence.
|
||||
* Fix deadlock with iptables with large ruleset.
|
||||
* docs: document --ns-hosts --to-ns and update --dns.
|
||||
* Use subprocess.check_output instead of run.
|
||||
* Fix potential deadlock condition in nft_get_handle.
|
||||
* auto-nets: retrieve routes only if using auto-nets.
|
||||
|
||||
|
||||
0.78.4 - 2018-04-02
|
||||
|
191
LICENSE
191
LICENSE
@ -1,13 +1,14 @@
|
||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991 Free Software Foundation, Inc.
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the library GPL. It is
|
||||
numbered 2 because it goes with version 2 of the ordinary GPL.]
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
@ -16,97 +17,109 @@ freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Library General Public License, applies to some
|
||||
specially designated Free Software Foundation software, and to any
|
||||
other libraries whose authors decide to use it. You can use it for
|
||||
your libraries, too.
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if
|
||||
you distribute copies of the library, or if you modify it.
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link a program with the library, you must provide
|
||||
complete object files to the recipients so that they can relink them
|
||||
with the library, after making changes to the library and recompiling
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
Our method of protecting your rights has two steps: (1) copyright
|
||||
the library, and (2) offer you this license which gives you legal
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
Also, for each distributor's protection, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
library. If the library is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original
|
||||
version, so that any problems introduced by others will not reflect on
|
||||
the original authors' reputations.
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that companies distributing free
|
||||
software will individually obtain patent licenses, thus in effect
|
||||
transforming the program into proprietary software. To prevent this,
|
||||
we have made it clear that any patent must be licensed for everyone's
|
||||
free use or not licensed at all.
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the ordinary
|
||||
GNU General Public License, which was designed for utility programs. This
|
||||
license, the GNU Library General Public License, applies to certain
|
||||
designated libraries. This license is quite different from the ordinary
|
||||
one; be sure to read it in full, and don't assume that anything in it is
|
||||
the same as in the ordinary license.
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
The reason we have a separate public license for some libraries is that
|
||||
they blur the distinction we usually make between modifying or adding to a
|
||||
program and simply using it. Linking a program with a library, without
|
||||
changing the library, is in some sense simply using the library, and is
|
||||
analogous to running a utility program or application program. However, in
|
||||
a textual and legal sense, the linked executable is a combined work, a
|
||||
derivative of the original library, and the ordinary General Public License
|
||||
treats it as such.
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
Because of this blurred distinction, using the ordinary General
|
||||
Public License for libraries did not effectively promote software
|
||||
sharing, because most developers did not use the libraries. We
|
||||
concluded that weaker conditions might promote sharing better.
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
However, unrestricted linking of non-free programs would deprive the
|
||||
users of those programs of all benefit from the free status of the
|
||||
libraries themselves. This Library General Public License is intended to
|
||||
permit developers of non-free programs to use free libraries, while
|
||||
preserving your freedom as a user of such programs to change the free
|
||||
libraries that are incorporated in them. (We have not seen how to achieve
|
||||
this as regards changes in header files, but we have achieved it as regards
|
||||
changes in the actual functions of the Library.) The hope is that this
|
||||
will lead to faster development of free libraries.
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, while the latter only
|
||||
works together with the library.
|
||||
|
||||
Note that it is possible for a library to be covered by the ordinary
|
||||
General Public License rather than by this special one.
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library which
|
||||
contains a notice placed by the copyright holder or other authorized
|
||||
party saying it may be distributed under the terms of this Library
|
||||
General Public License (also called "this License"). Each licensee is
|
||||
addressed as "you".
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
@ -255,7 +268,7 @@ distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also compile or
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
@ -282,23 +295,31 @@ of these things:
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Accompany the work with a written offer, valid for at
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
c) If distribution of the work is made by offering access to copy
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
d) Verify that the user has already received a copy of these
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the source code distributed need not include anything that is normally
|
||||
distributed (in either source or binary form) with the major
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
@ -347,7 +368,7 @@ Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
@ -390,7 +411,7 @@ excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Library General Public License from time to time.
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
@ -453,16 +474,16 @@ convey the exclusion of warranty; and each file should have at least the
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Library General Public
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2 of the License, or (at your option) any later version.
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Library General Public License for more details.
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Library General Public
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
|
53
README.rst
53
README.rst
@ -23,16 +23,41 @@ common case:
|
||||
|
||||
- You can't use openssh's PermitTunnel feature because
|
||||
it's disabled by default on openssh servers; plus it does
|
||||
TCP-over-TCP, which has terrible performance (see below).
|
||||
TCP-over-TCP, which has `terrible performance`_.
|
||||
|
||||
.. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html
|
||||
|
||||
Obtaining sshuttle
|
||||
------------------
|
||||
|
||||
- Ubuntu 16.04 or later::
|
||||
|
||||
apt-get install sshuttle
|
||||
|
||||
- Debian stretch or later::
|
||||
|
||||
apt-get install sshuttle
|
||||
|
||||
- Arch Linux::
|
||||
|
||||
pacman -S sshuttle
|
||||
|
||||
- Fedora::
|
||||
|
||||
dnf install sshuttle
|
||||
|
||||
- openSUSE::
|
||||
|
||||
zypper in sshuttle
|
||||
|
||||
- Gentoo::
|
||||
|
||||
emerge -av net-proxy/sshuttle
|
||||
|
||||
- NixOS::
|
||||
|
||||
nix-env -iA nixos.sshuttle
|
||||
|
||||
- From PyPI::
|
||||
|
||||
sudo pip install sshuttle
|
||||
@ -43,6 +68,18 @@ Obtaining sshuttle
|
||||
cd sshuttle
|
||||
sudo ./setup.py install
|
||||
|
||||
- FreeBSD::
|
||||
|
||||
# ports
|
||||
cd /usr/ports/net/py-sshuttle && make install clean
|
||||
# pkg
|
||||
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.
|
||||
|
||||
- From PyPI::
|
||||
@ -63,11 +100,21 @@ It is also possible to install into a virtualenv as a non-root user.
|
||||
|
||||
brew install sshuttle
|
||||
|
||||
- Nix::
|
||||
|
||||
nix-env -iA nixpkgs.sshuttle
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
The documentation for the stable version is available at:
|
||||
http://sshuttle.readthedocs.org/
|
||||
https://sshuttle.readthedocs.org/
|
||||
|
||||
The documentation for the latest development version is available at:
|
||||
http://sshuttle.readthedocs.org/en/latest/
|
||||
https://sshuttle.readthedocs.org/en/latest/
|
||||
|
||||
|
||||
Running as a service
|
||||
--------------------
|
||||
Sshuttle can also be run as a service and configured using a config management system:
|
||||
https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe
|
||||
|
@ -1,5 +1,5 @@
|
||||
exclude_dirs:
|
||||
- sshuttle/tests
|
||||
- tests
|
||||
skips:
|
||||
- B101
|
||||
- B104
|
||||
|
84
bin/sudoers-add
Executable file
84
bin/sudoers-add
Executable file
@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# William Mantly <wmantly@gmail.com>
|
||||
# MIT License
|
||||
# https://github.com/wmantly/sudoers-add
|
||||
|
||||
NEWLINE=$'\n'
|
||||
CONTENT=""
|
||||
ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")"
|
||||
|
||||
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
|
||||
echo "Usage: $ME [file_path] [sudoers-file-name]"
|
||||
echo "Usage: [content] | $ME sudoers-file-name"
|
||||
echo "This will take a sudoers config validate it and add it to /etc/sudoers.d/{sudoers-file-name}"
|
||||
echo "The config can come from a file, first usage example or piped in second example."
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" == "" ]; then
|
||||
(>&2 echo "This command take at lest one argument. See $ME --help")
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$2" == "" ]; then
|
||||
FILE_NAME=$1
|
||||
shift
|
||||
else
|
||||
FILE_NAME=$2
|
||||
fi
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while read -r line
|
||||
do
|
||||
CONTENT+="${line}${NEWLINE}"
|
||||
done < "${1:-/dev/stdin}"
|
||||
|
||||
if [ "$CONTENT" == "" ]; then
|
||||
(>&2 echo "No config content specified. See $ME --help")
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$FILE_NAME" == "" ]; then
|
||||
(>&2 echo "No sudoers file name specified. See $ME --help")
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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
|
||||
umask 077
|
||||
TEMP_FILE=$(mktemp)
|
||||
echo "$CONTENT" > "$TEMP_FILE"
|
||||
|
||||
# Make sure the content is valid
|
||||
visudo_STDOUT=$(visudo -c -f "$TEMP_FILE" 2>&1)
|
||||
visudo_code=$?
|
||||
# The temp file is no longer needed
|
||||
rm "$TEMP_FILE"
|
||||
|
||||
if [ $visudo_code -eq 0 ]; then
|
||||
echo "$CONTENT" > "$FILE_NAME"
|
||||
chmod 0440 "$FILE_NAME"
|
||||
echo "The sudoers file $FILE_NAME has been successfully created!"
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "Invalid sudoers config!"
|
||||
echo "$visudo_STDOUT"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
10
conftest.py
10
conftest.py
@ -1,10 +0,0 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
good_python = sys.version_info >= (3, 5)
|
||||
else:
|
||||
good_python = sys.version_info >= (2, 7)
|
||||
|
||||
collect_ignore = []
|
||||
if not good_python:
|
||||
collect_ignore.append("sshuttle/tests/client")
|
11
docs/chromeos.rst
Normal file
11
docs/chromeos.rst
Normal file
@ -0,0 +1,11 @@
|
||||
Google ChromeOS
|
||||
===============
|
||||
|
||||
Currently there is no built in support for running sshuttle directly on
|
||||
Google ChromeOS/Chromebooks.
|
||||
|
||||
What we can really do is to create a Linux VM with Crostini. In the default
|
||||
stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and
|
||||
it just works, as do xterms and ssvncviewer etc.
|
||||
|
||||
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide
|
@ -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
|
||||
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
||||
safe.
|
||||
|
||||
|
@ -26,4 +26,3 @@ Indices and tables
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
@ -5,8 +5,20 @@ Installation
|
||||
|
||||
pip install sshuttle
|
||||
|
||||
- Debian package manager::
|
||||
|
||||
sudo apt install sshuttle
|
||||
|
||||
- Clone::
|
||||
|
||||
git clone https://github.com/sshuttle/sshuttle.git
|
||||
cd sshuttle
|
||||
./setup.py install
|
||||
|
||||
|
||||
Optionally after installation
|
||||
-----------------------------
|
||||
|
||||
- Add to sudoers file::
|
||||
|
||||
sshuttle --sudoers
|
||||
|
277
docs/manpage.rst
277
docs/manpage.rst
@ -4,14 +4,14 @@ sshuttle
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
**sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\>
|
||||
**sshuttle** [*options*] **-r** *[username@]sshserver[:port]* \<*subnets* ...\>
|
||||
|
||||
|
||||
Description
|
||||
-----------
|
||||
:program:`sshuttle` allows you to create a VPN connection from your
|
||||
machine to any remote server that you can connect to via
|
||||
ssh, as long as that server has python 2.3 or higher.
|
||||
machine to any remote server that you can connect to via ssh, as long
|
||||
as that server has a sufficiently new Python installation.
|
||||
|
||||
To work, you must have root access on the local machine,
|
||||
but you can have a normal account on the server.
|
||||
@ -28,29 +28,34 @@ Options
|
||||
-------
|
||||
.. program:: sshuttle
|
||||
|
||||
.. option:: subnets
|
||||
.. option:: <subnets>
|
||||
|
||||
A list of subnets to route over the VPN, in the form
|
||||
``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
|
||||
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
|
||||
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
|
||||
netmask), and 0/0 ('just route everything through the
|
||||
VPN'). Any of the previous examples are also valid if you append
|
||||
a port or a port range, so 1.2.3.4:8000 will only tunnel traffic
|
||||
that has as the destination port 8000 of 1.2.3.4 and
|
||||
1.2.3.0/24:8000-9000 will tunnel traffic going to any port between
|
||||
8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet.
|
||||
It is also possible to use a name in which case the first IP it resolves
|
||||
to during startup will be routed over the VPN. Valid examples are
|
||||
example.com, example.com:8000 and example.com:8000-9000.
|
||||
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 netmask).
|
||||
Specify subnets 0/0 to match all IPv4 addresses and ::/0 to match
|
||||
all IPv6 addresses. Any of the previous examples are also valid if
|
||||
you append a port or a port range, so 1.2.3.4:8000 will only
|
||||
tunnel traffic that has as the destination port 8000 of 1.2.3.4
|
||||
and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port
|
||||
between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24
|
||||
subnet. A hostname can be provided instead of an IP address. If
|
||||
the hostname resolves to multiple IPs, all of the IPs are
|
||||
included. If a width is provided with a hostname, the width is
|
||||
applied to all of the hostnames IPs (if they are all either IPv4
|
||||
or IPv6). Widths cannot be supplied to hostnames that resolve to
|
||||
both IPv4 and IPv6. Valid examples are example.com,
|
||||
example.com:8000, example.com/24, example.com/24:8000 and
|
||||
example.com:8000-9000.
|
||||
|
||||
.. option:: --method [auto|nat|tproxy|pf]
|
||||
.. option:: --method <auto|nat|nft|tproxy|pf|ipfw>
|
||||
|
||||
Which firewall method should sshuttle use? For auto, sshuttle attempts to
|
||||
guess the appropriate method depending on what it can find in PATH. The
|
||||
default value is auto.
|
||||
|
||||
.. option:: -l, --listen=[ip:]port
|
||||
.. option:: -l <[ip:]port>, --listen=<[ip:]port>
|
||||
|
||||
Use this ip address and port number as the transparent
|
||||
proxy port. By default :program:`sshuttle` finds an available
|
||||
@ -64,8 +69,9 @@ Options
|
||||
You can use any name resolving to an IP address of the machine running
|
||||
:program:`sshuttle`, e.g. ``--listen localhost``.
|
||||
|
||||
For the tproxy and pf methods this can be an IPv6 address. Use this option
|
||||
twice if required, to provide both IPv4 and IPv6 addresses.
|
||||
For the nft, tproxy and pf methods this can be an IPv6 address. Use
|
||||
this option with comma separated values if required, to provide both
|
||||
IPv4 and IPv6 addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``.
|
||||
|
||||
.. option:: -H, --auto-hosts
|
||||
|
||||
@ -83,6 +89,13 @@ Options
|
||||
few subnets over the VPN, you probably would prefer to
|
||||
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
|
||||
|
||||
In addition to the subnets provided on the command
|
||||
@ -91,25 +104,57 @@ Options
|
||||
are taken automatically from the server's routing
|
||||
table.
|
||||
|
||||
This feature does not detect IPv6 routes. Specify IPv6 subnets
|
||||
manually. For example, specify the ``::/0`` subnet on the command
|
||||
line to route all IPv6 traffic.
|
||||
|
||||
.. option:: --dns
|
||||
|
||||
Capture local DNS requests and forward to the remote DNS
|
||||
server.
|
||||
server. All queries to any of the local system's DNS
|
||||
servers (/etc/resolv.conf and, if it exists,
|
||||
/run/systemd/resolve/resolv.conf) will be intercepted and
|
||||
resolved on the remote side of the tunnel instead, there
|
||||
using the DNS specified via the :option:`--to-ns` option,
|
||||
if specified. Only plain DNS traffic sent to these servers
|
||||
on port 53 are captured.
|
||||
|
||||
.. option:: --ns-hosts=<server1[,server2[,server3[...]]]>
|
||||
|
||||
Capture local DNS requests to the specified server(s)
|
||||
and forward to the remote DNS server. Contrary to the
|
||||
:option:`--dns` option, this flag allows to specify the
|
||||
DNS server(s) the queries to which to intercept,
|
||||
instead of intercepting all DNS traffic on the local
|
||||
machine. This can be useful when only certain DNS
|
||||
requests should be resolved on the remote side of the
|
||||
tunnel, e.g. in combination with dnsmasq.
|
||||
|
||||
.. option:: --to-ns=<server>
|
||||
|
||||
The DNS to forward requests to when remote DNS
|
||||
resolution is enabled. If not given, sshuttle will
|
||||
simply resolve using the system configured resolver on
|
||||
the remote side (via /etc/resolv.conf on the remote
|
||||
side).
|
||||
|
||||
.. option:: --python
|
||||
|
||||
Specify the name/path of the remote python interpreter.
|
||||
The default is just ``python``, which means to use the
|
||||
default python interpreter on the remote system's PATH.
|
||||
Specify the name/path of the remote python interpreter. The
|
||||
default is to use ``python3`` (or ``python``, if ``python3``
|
||||
fails) in the remote system's PATH.
|
||||
|
||||
.. option:: -r, --remote=[username@]sshserver[:port]
|
||||
.. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
|
||||
|
||||
The remote hostname and optional username and ssh
|
||||
port number to use for connecting to the remote server.
|
||||
For example, example.com, testuser@example.com,
|
||||
testuser@example.com:2222, or example.com:2244.
|
||||
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, --exclude=subnet
|
||||
.. option:: -x <subnet>, --exclude=<subnet>
|
||||
|
||||
Explicitly exclude this subnet from forwarding. The
|
||||
format of this option is the same as the ``<subnets>``
|
||||
@ -118,7 +163,7 @@ Options
|
||||
``0/0 -x 1.2.3.0/24`` to forward everything except the
|
||||
local subnet over the VPN, for example.
|
||||
|
||||
.. option:: -X, --exclude-from=file
|
||||
.. option:: -X <file>, --exclude-from=<file>
|
||||
|
||||
Exclude the subnets specified in a file, one subnet per
|
||||
line. Useful when you have lots of subnets to exclude.
|
||||
@ -140,7 +185,7 @@ Options
|
||||
|
||||
A comma-separated list of hostnames to use to
|
||||
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
|
||||
if you use this option to give it a few names to start
|
||||
from.
|
||||
@ -166,18 +211,30 @@ Options
|
||||
control feature, maximizing bandwidth usage. Use at
|
||||
your own risk.
|
||||
|
||||
.. option:: --latency-buffer-size
|
||||
|
||||
Set the size of the buffer used in latency control. The
|
||||
default is ``32768``. Changing this option allows a compromise
|
||||
to be made between latency and bandwidth without completely
|
||||
disabling latency control (with :option:`--no-latency-control`).
|
||||
|
||||
.. option:: -D, --daemon
|
||||
|
||||
Automatically fork into the background after connecting
|
||||
to the remote server. Implies :option:`--syslog`.
|
||||
|
||||
.. option:: -s <file>, --subnets=<file>
|
||||
|
||||
Include the subnets specified in a file instead of on the
|
||||
command line. One subnet per line.
|
||||
|
||||
.. option:: --syslog
|
||||
|
||||
after connecting, send all log messages to the
|
||||
:manpage:`syslog(3)` service instead of stderr. This is
|
||||
implicit if you use :option:`--daemon`.
|
||||
|
||||
.. option:: --pidfile=pidfilename
|
||||
.. option:: --pidfile=<pidfilename>
|
||||
|
||||
when using :option:`--daemon`, save :program:`sshuttle`'s pid to
|
||||
*pidfilename*. The default is ``sshuttle.pid`` in the
|
||||
@ -185,7 +242,8 @@ Options
|
||||
|
||||
.. option:: --disable-ipv6
|
||||
|
||||
If using tproxy or pf methods, this will disable IPv6 support.
|
||||
Disable IPv6 support for methods that support it (nft, tproxy, and
|
||||
pf).
|
||||
|
||||
.. option:: --firewall
|
||||
|
||||
@ -204,17 +262,49 @@ Options
|
||||
makes it a lot easier to debug and test the :option:`--auto-hosts`
|
||||
feature.
|
||||
|
||||
.. option:: --sudoers
|
||||
|
||||
sshuttle will auto generate the proper sudoers.d config file and add it.
|
||||
Once this is completed, sshuttle will exit and tell the user if
|
||||
it succeed or not. Do not call this options with sudo, it may generate a
|
||||
incorrect config file.
|
||||
|
||||
.. option:: --sudoers-no-modify
|
||||
|
||||
sshuttle will auto generate the proper sudoers.d config and print it to
|
||||
stdout. The option will not modify the system at all.
|
||||
|
||||
.. option:: --sudoers-user
|
||||
|
||||
Set the user name or group with %group_name for passwordless operation.
|
||||
Default is the current user.set ALL for all users. Only works with
|
||||
--sudoers or --sudoers-no-modify option.
|
||||
|
||||
.. option:: --sudoers-filename
|
||||
|
||||
Set the file name for the sudoers.d file to be added. Default is
|
||||
"sshuttle_auto". Only works with --sudoers.
|
||||
|
||||
.. option:: -t <mark>, --tmark=<mark>
|
||||
|
||||
An option used by the tproxy method: Use the specified traffic
|
||||
mark. The mark must be a hexadecimal value. Defaults to 0x01.
|
||||
|
||||
.. option:: --version
|
||||
|
||||
Print program version.
|
||||
|
||||
|
||||
Configuration File
|
||||
------------------
|
||||
All the options described above can optionally be specified in a configuration
|
||||
file.
|
||||
|
||||
To run :program:`sshuttle` with options defined in, e.g., `/etc/ssshuttle.conf`
|
||||
To run :program:`sshuttle` with options defined in, e.g., `/etc/sshuttle.conf`
|
||||
just pass the path to the file preceded by the `@` character, e.g.
|
||||
:option:`@/etc/ssshuttle.conf`.
|
||||
`@/etc/sshuttle.conf`.
|
||||
|
||||
When running :program:`sshuttle` with options defined in a configuratio file,
|
||||
When running :program:`sshuttle` with options defined in a configuration file,
|
||||
options can still be passed via the command line in addition to what is
|
||||
defined in the file. If a given option is defined both in the file and in
|
||||
the command line, the value in the command line will take precedence.
|
||||
@ -227,54 +317,107 @@ Arguments read from a file must be one per line, as shown below::
|
||||
--option2
|
||||
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
|
||||
--------
|
||||
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.
|
||||
Listening on ('0.0.0.0', 12300).
|
||||
$ sshuttle -r example.com -x example.com:22 0/0
|
||||
|
||||
Starting sshuttle proxy (version ...).
|
||||
[local sudo] Password:
|
||||
firewall manager ready.
|
||||
c : connecting to server...
|
||||
s: available routes:
|
||||
s: 192.168.42.0/24
|
||||
c : connected.
|
||||
firewall manager: starting transproxy.
|
||||
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
|
||||
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
|
||||
...etc...
|
||||
fw: Starting firewall with Python version 3.9.5
|
||||
fw: ready method name nat.
|
||||
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 : (<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
|
||||
firewall manager: undoing changes.
|
||||
KeyboardInterrupt
|
||||
c : Keyboard interrupt: exiting.
|
||||
c : SW#8:192.168.42.121:47523: deleting
|
||||
c : SW#6:192.168.42.106:50035: deleting
|
||||
c : SW'unknown':Mux#1: deleting (1 remain)
|
||||
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::
|
||||
|
||||
$ sshuttle -vNHr example.org
|
||||
|
||||
Starting sshuttle proxy.
|
||||
Listening on ('0.0.0.0', 12300).
|
||||
firewall manager ready.
|
||||
c : connecting to server...
|
||||
$ sshuttle -vNHr example.com -x example.com:22
|
||||
Starting sshuttle proxy (version ...).
|
||||
[local sudo] Password:
|
||||
fw: Starting firewall with Python version 3.9.5
|
||||
fw: ready method name nat.
|
||||
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: 77.141.99.0/24
|
||||
c : connected.
|
||||
c : seed_hosts: []
|
||||
firewall manager: starting transproxy.
|
||||
hostwatch: Found: testbox1: 1.2.3.4
|
||||
hostwatch: Found: mytest2: 5.6.7.8
|
||||
hostwatch: Found: domaincontroller: 99.1.2.3
|
||||
fw: setting up.
|
||||
fw: iptables -w -t nat -N sshuttle-12300
|
||||
fw: iptables -w -t nat -F sshuttle-12300
|
||||
...
|
||||
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
|
||||
^C
|
||||
firewall manager: undoing changes.
|
||||
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::
|
||||
|
||||
@ -298,9 +441,7 @@ Example configuration file::
|
||||
Discussion
|
||||
----------
|
||||
When it starts, :program:`sshuttle` creates an ssh session to the
|
||||
server specified by the ``-r`` option. If ``-r`` is omitted,
|
||||
it will start both its client and server locally, which is
|
||||
sometimes useful for testing.
|
||||
server specified by the ``-r`` option.
|
||||
|
||||
After connecting to the remote server, :program:`sshuttle` uploads its
|
||||
(python) source code to the remote end and executes it
|
||||
|
8
docs/openwrt.rst
Normal file
8
docs/openwrt.rst
Normal file
@ -0,0 +1,8 @@
|
||||
OpenWRT
|
||||
========
|
||||
|
||||
Run::
|
||||
|
||||
opkg install python3 python3-pip iptables-mod-extra iptables-mod-nat-extra iptables-mod-ipopt
|
||||
python3 /usr/bin/pip3 install sshuttle
|
||||
sshuttle -l 0.0.0.0 -r <IP> -x 192.168.1.1 0/0
|
@ -6,5 +6,7 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
chromeos
|
||||
tproxy
|
||||
windows
|
||||
openwrt
|
||||
|
@ -6,7 +6,7 @@ Client side Requirements
|
||||
|
||||
- sudo, or root access on your client machine.
|
||||
(The server doesn't need admin access.)
|
||||
- Python 2.7 or Python 3.5.
|
||||
- Python 3.6 or greater.
|
||||
|
||||
|
||||
Linux with NAT method
|
||||
@ -15,30 +15,36 @@ Supports:
|
||||
|
||||
* IPv4 TCP
|
||||
* IPv4 DNS
|
||||
* IPv6 TCP
|
||||
* IPv6 DNS
|
||||
|
||||
Requires:
|
||||
|
||||
* iptables DNAT, REDIRECT, and ttl modules.
|
||||
* iptables DNAT and REDIRECT modules. ip6tables for IPv6.
|
||||
|
||||
Linux with nft method
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
Supports
|
||||
|
||||
* IPv4 TCP
|
||||
* IPv4 DNS
|
||||
* IPv6 TCP
|
||||
* IPv6 DNS
|
||||
|
||||
Requires:
|
||||
|
||||
* nftables
|
||||
|
||||
Linux with TPROXY method
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Supports:
|
||||
|
||||
* IPv4 TCP
|
||||
* IPv4 UDP (requires ``recvmsg`` - see below)
|
||||
* IPv6 DNS (requires ``recvmsg`` - see below)
|
||||
* IPv4 UDP
|
||||
* IPv4 DNS
|
||||
* IPv6 TCP
|
||||
* IPv6 UDP (requires ``recvmsg`` - see below)
|
||||
* IPv6 DNS (requires ``recvmsg`` - see below)
|
||||
|
||||
.. _PyXAPI: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
|
||||
|
||||
Full UDP or DNS support with the TPROXY method requires the ``recvmsg()``
|
||||
syscall. This is not available in Python 2, however it is in Python 3.5 and
|
||||
later. Under Python 2 you might find it sufficient to install PyXAPI_ in
|
||||
order to get the ``recvmsg()`` function. See :doc:`tproxy` for more
|
||||
information.
|
||||
* IPv6 UDP
|
||||
* IPv6 DNS
|
||||
|
||||
|
||||
MacOS / FreeBSD / OpenBSD / pfSense
|
||||
@ -65,16 +71,13 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
|
||||
|
||||
Server side Requirements
|
||||
------------------------
|
||||
The server can run in any version of Python between 2.4 and 3.6.
|
||||
However it is recommended that you use Python 2.7, Python 3.5 or later whenever
|
||||
possible as support for older versions might be dropped in the future.
|
||||
|
||||
- Python 3.6 or greater.
|
||||
|
||||
|
||||
Additional Suggested Software
|
||||
-----------------------------
|
||||
|
||||
- You may want to use autossh, available in various package management
|
||||
systems.
|
||||
- If you are using systemd, sshuttle can notify it when the connection to
|
||||
the remote end is established and the firewall rules are installed. For
|
||||
this feature to work you must configure the process start-up type for the
|
||||
|
@ -1,6 +1,6 @@
|
||||
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:
|
||||
|
||||
@ -8,9 +8,12 @@ There are some things you need to consider for TPROXY to work:
|
||||
done once after booting up::
|
||||
|
||||
ip route add local default dev lo table 100
|
||||
ip rule add fwmark 1 lookup 100
|
||||
ip rule add fwmark {TMARK} lookup 100
|
||||
ip -6 route add local default dev lo table 100
|
||||
ip -6 rule add fwmark 1 lookup 100
|
||||
ip -6 rule add fwmark {TMARK} lookup 100
|
||||
|
||||
where {TMARK} is the identifier mark passed with -t or --tmark flag
|
||||
as a hexadecimal string (default value is '0x01').
|
||||
|
||||
- The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6
|
||||
routes manually. e.g. by adding ``'::/0'`` to the end of the command line.
|
||||
@ -23,11 +26,6 @@ There are some things you need to consider for TPROXY to work:
|
||||
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
|
||||
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.
|
||||
|
||||
- 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
|
||||
program that was the best of all worlds. And here we are, at last.
|
||||
You're welcome.
|
||||
|
||||
|
@ -11,6 +11,10 @@ Forward all traffic::
|
||||
sshuttle -r username@sshserver 0.0.0.0/0
|
||||
|
||||
- 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
|
||||
the :option:`sshuttle --method` parameter.
|
||||
@ -20,6 +24,11 @@ Forward all traffic::
|
||||
|
||||
sshuttle -r username@sshserver 0/0
|
||||
|
||||
|
||||
- For 'My VPN broke and need a temporary solution FAST to access local IPv4 addresses'::
|
||||
|
||||
sshuttle --dns -NHr username@sshserver 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||
|
||||
If you would also like your DNS queries to be proxied
|
||||
through the DNS server of the server you are connect to::
|
||||
|
||||
@ -60,3 +69,46 @@ the data back and forth through ssh.
|
||||
Fun, right? A poor man's instant VPN, and you don't even have to have
|
||||
admin access on the server.
|
||||
|
||||
Sudoers File
|
||||
------------
|
||||
sshuttle can auto-generate the proper sudoers.d file using the current user
|
||||
for Linux and OSX. Doing this will allow sshuttle to run without asking for
|
||||
the local sudo password and to give users who do not have sudo access
|
||||
ability to run sshuttle::
|
||||
|
||||
sshuttle --sudoers
|
||||
|
||||
DO NOT run this command with sudo, it will ask for your sudo password when
|
||||
it is needed.
|
||||
|
||||
A costume user or group can be set with the :
|
||||
option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid
|
||||
values for this vary based on how your system is configured. Values such as
|
||||
usernames, groups pre-pended with `%` and sudoers user aliases will work. See
|
||||
the sudoers manual for more information on valid user specif actions.
|
||||
The options must be used with `--sudoers`::
|
||||
|
||||
sshuttle --sudoers --sudoers-user mike
|
||||
sshuttle --sudoers --sudoers-user %sudo
|
||||
|
||||
The name of the file to be added to sudoers.d can be configured as well. This
|
||||
is mostly not necessary but can be useful for giving more than one user
|
||||
access to sshuttle. The default is `sshuttle_auto`::
|
||||
|
||||
sshuttle --sudoer --sudoers-filename sshuttle_auto_mike
|
||||
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
|
||||
|
||||
You can also see what configuration will be added to your system without
|
||||
modifying anything. This can be helpful if the auto feature does not work, or
|
||||
you want more control. This option also works with `--sudoers-username`.
|
||||
`--sudoers-filename` has no effect with this option::
|
||||
|
||||
sshuttle --sudoers-no-modify
|
||||
|
||||
This will simply sprint the generated configuration to STDOUT. Example::
|
||||
|
||||
08:40 PM william$ sshuttle --sudoers-no-modify
|
||||
|
||||
Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall
|
||||
|
||||
william ALL=NOPASSWD: SSHUTTLE304
|
||||
|
@ -1,4 +1,5 @@
|
||||
-r requirements.txt
|
||||
pytest==3.4.2
|
||||
mock==2.0.0
|
||||
flake8==3.5.0
|
||||
pytest==6.2.5
|
||||
pytest-cov==3.0.0
|
||||
flake8==4.0.1
|
||||
pyflakes==2.4.0
|
||||
|
@ -1 +1,2 @@
|
||||
setuptools-scm==1.15.6
|
||||
setuptools-scm==6.4.2
|
||||
Sphinx==4.3.2
|
||||
|
5
run
5
run
@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
export PYTHONPATH="$(dirname $0):$PYTHONPATH"
|
||||
export PYTHONPATH="$(dirname "$0"):$PYTHONPATH"
|
||||
export PATH="$(dirname "$0")/bin:$PATH"
|
||||
|
||||
python_best_version() {
|
||||
if [ -x "$(command -v python3)" ] &&
|
||||
python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
|
||||
exec python3 "$@"
|
||||
elif [ -x "$(command -v python2.7)" ]; then
|
||||
exec python2.7 "$@"
|
||||
else
|
||||
exec python "$@"
|
||||
fi
|
||||
|
@ -7,3 +7,11 @@ universal = 1
|
||||
[upload]
|
||||
sign=true
|
||||
identity=0x1784577F811F6EAC
|
||||
|
||||
[flake8]
|
||||
count=true
|
||||
show-source=true
|
||||
statistics=true
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=sshuttle --cov-branch --cov-report=term-missing
|
||||
|
18
setup.py
18
setup.py
@ -41,6 +41,7 @@ setup(
|
||||
packages=find_packages(),
|
||||
license="LGPL2.1+",
|
||||
long_description=open('README.rst').read(),
|
||||
long_description_content_type="text/x-rst",
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
@ -48,15 +49,26 @@ setup(
|
||||
"License :: OSI Approved :: "
|
||||
"GNU Lesser General Public License v2 or later (LGPLv2+)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: System :: Networking",
|
||||
],
|
||||
scripts=['bin/sudoers-add'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'sshuttle = sshuttle.cmdline:main',
|
||||
],
|
||||
},
|
||||
tests_require=['pytest', 'pytest-runner', 'mock'],
|
||||
python_requires='>=3.6',
|
||||
install_requires=[
|
||||
],
|
||||
tests_require=[
|
||||
'pytest',
|
||||
'pytest-cov',
|
||||
'pytest-runner',
|
||||
'flake8',
|
||||
],
|
||||
keywords="ssh vpn",
|
||||
)
|
||||
|
@ -1,21 +1,28 @@
|
||||
import sys
|
||||
import zlib
|
||||
import imp
|
||||
import types
|
||||
import platform
|
||||
|
||||
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()
|
||||
while 1:
|
||||
name = sys.stdin.readline().strip()
|
||||
if name:
|
||||
# python2 compat: in python2 sys.stdin.readline().strip() -> str
|
||||
# in python3 sys.stdin.readline().strip() -> bytes
|
||||
# (see #481)
|
||||
if sys.version_info >= (3, 0):
|
||||
name = name.decode("ASCII")
|
||||
|
||||
nbytes = int(sys.stdin.readline())
|
||||
if verbosity >= 2:
|
||||
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
||||
sys.stderr.write(' s: assembling %r (%d bytes)\r\n'
|
||||
% (name, nbytes))
|
||||
content = z.decompress(sys.stdin.read(nbytes))
|
||||
|
||||
module = imp.new_module(name)
|
||||
module = types.ModuleType(name)
|
||||
parents = name.rsplit(".", 1)
|
||||
if len(parents) == 2:
|
||||
parent, parent_name = parents
|
||||
@ -30,9 +37,13 @@ while 1:
|
||||
sys.stderr.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
import sshuttle.helpers
|
||||
# import can only happen once the code has been transferred to
|
||||
# the server. 'noqa: E402' excludes these lines from QA checks.
|
||||
import sshuttle.helpers # noqa: E402
|
||||
sshuttle.helpers.verbose = verbosity
|
||||
|
||||
import sshuttle.cmdline_options as options
|
||||
from sshuttle.server import main
|
||||
main(options.latency_control, options.auto_hosts, options.to_nameserver)
|
||||
import sshuttle.cmdline_options as options # noqa: E402
|
||||
from sshuttle.server import main # noqa: E402
|
||||
main(options.latency_control, options.latency_buffer_size,
|
||||
options.auto_hosts, options.to_nameserver,
|
||||
options.auto_nets)
|
||||
|
@ -3,44 +3,36 @@ import re
|
||||
import signal
|
||||
import time
|
||||
import subprocess as ssubprocess
|
||||
import sshuttle.helpers as helpers
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
|
||||
import sshuttle.helpers as helpers
|
||||
import sshuttle.ssnet as ssnet
|
||||
import sshuttle.ssh as ssh
|
||||
import sshuttle.ssyslog as ssyslog
|
||||
import sys
|
||||
import platform
|
||||
import sshuttle.sdnotify as sdnotify
|
||||
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
||||
resolvconf_nameservers
|
||||
resolvconf_nameservers, which
|
||||
from sshuttle.methods import get_method, Features
|
||||
from sshuttle import __version__
|
||||
try:
|
||||
from pwd import getpwnam
|
||||
except ImportError:
|
||||
getpwnam = None
|
||||
|
||||
try:
|
||||
# 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
|
||||
import socket
|
||||
|
||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
||||
|
||||
|
||||
def got_signal(signum, frame):
|
||||
log('exiting on signal %d\n' % signum)
|
||||
log('exiting on signal %d' % signum)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Filename of the pidfile created by the sshuttle client.
|
||||
_pidname = None
|
||||
|
||||
|
||||
@ -76,13 +68,25 @@ def check_daemon(pidfile):
|
||||
|
||||
|
||||
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():
|
||||
os._exit(0)
|
||||
os.setsid()
|
||||
if os.fork():
|
||||
os._exit(0)
|
||||
|
||||
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
|
||||
# Write pid to the pidfile.
|
||||
try:
|
||||
os.write(outfd, b'%d\n' % os.getpid())
|
||||
finally:
|
||||
@ -93,7 +97,7 @@ def daemonize():
|
||||
# be deleted.
|
||||
signal.signal(signal.SIGTERM, got_signal)
|
||||
|
||||
si = open('/dev/null', 'r+')
|
||||
si = open(os.devnull, 'r+')
|
||||
os.dup2(si.fileno(), 0)
|
||||
os.dup2(si.fileno(), 1)
|
||||
si.close()
|
||||
@ -148,7 +152,7 @@ class MultiListener:
|
||||
try:
|
||||
self.v4.listen(backlog)
|
||||
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.
|
||||
if e.errno == errno.EADDRINUSE and self.v6:
|
||||
self.v4 = None
|
||||
@ -173,74 +177,125 @@ class MultiListener:
|
||||
assert(self.bind_called)
|
||||
if self.v6:
|
||||
listenip = self.v6.getsockname()
|
||||
debug1('%s listening on %r.\n' % (what, listenip))
|
||||
debug2('%s listening with %r.\n' % (what, self.v6))
|
||||
debug1('%s listening on %r.' % (what, listenip))
|
||||
debug2('%s listening with %r.' % (what, self.v6))
|
||||
if self.v4:
|
||||
listenip = self.v4.getsockname()
|
||||
debug1('%s listening on %r.\n' % (what, listenip))
|
||||
debug2('%s listening with %r.\n' % (what, self.v4))
|
||||
debug1('%s listening on %r.' % (what, listenip))
|
||||
debug2('%s listening with %r.' % (what, self.v4))
|
||||
|
||||
|
||||
class FirewallClient:
|
||||
|
||||
def __init__(self, method_name):
|
||||
def __init__(self, method_name, sudo_pythonpath):
|
||||
self.auto_nets = []
|
||||
python_path = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
argvbase = ([sys.executable, sys.argv[0]] +
|
||||
['-v'] * (helpers.verbose or 0) +
|
||||
['--method', method_name] +
|
||||
['--firewall'])
|
||||
if ssyslog._p:
|
||||
argvbase += ['--syslog']
|
||||
argv_tries = [
|
||||
['sudo', '-p', '[local sudo] Password: ', '/usr/bin/env',
|
||||
('PYTHONPATH=%s' % python_path)] + argvbase,
|
||||
argvbase
|
||||
]
|
||||
|
||||
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
|
||||
# because stupid Linux 'su' requires that stdin be attached to a tty.
|
||||
# Instead, attach a *bidirectional* socket to its stdout, and use
|
||||
# that for talking in both directions.
|
||||
# A list of commands that we can try to run to start the firewall.
|
||||
argv_tries = []
|
||||
|
||||
if os.getuid() == 0: # No need to elevate privileges
|
||||
argv_tries.append(argvbase)
|
||||
else:
|
||||
# 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
|
||||
|
||||
# For clarity, try to replace executable name with the
|
||||
# full path.
|
||||
doas_path = which("doas")
|
||||
if doas_path:
|
||||
doas_cmd[0] = doas_path
|
||||
sudo_path = which("sudo")
|
||||
if sudo_path:
|
||||
sudo_cmd[0] = sudo_path
|
||||
|
||||
# sudo_pythonpath indicates if we should set the
|
||||
# PYTHONPATH environment variable when elevating
|
||||
# privileges. This can be adjusted with the
|
||||
# --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
|
||||
|
||||
# If we can find doas and not sudo or if we are on
|
||||
# OpenBSD, try using doas first.
|
||||
if (doas_path and not sudo_path) or \
|
||||
platform.platform().startswith('OpenBSD'):
|
||||
argv_tries = [doas_cmd, sudo_cmd, argvbase]
|
||||
else:
|
||||
argv_tries = [sudo_cmd, doas_cmd, argvbase]
|
||||
|
||||
# Try all commands in argv_tries in order. If a command
|
||||
# produces an error, try the next one. If command is
|
||||
# successful, set 'success' variable and break.
|
||||
success = False
|
||||
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()
|
||||
e = None
|
||||
if os.getuid() == 0:
|
||||
argv_tries = argv_tries[-1:] # last entry only
|
||||
for argv in argv_tries:
|
||||
|
||||
try:
|
||||
if argv[0] == 'su':
|
||||
sys.stderr.write('[local su] ')
|
||||
debug1("Starting firewall manager with command: %r" % argv)
|
||||
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
||||
e = None
|
||||
break
|
||||
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
||||
except OSError as e:
|
||||
pass
|
||||
# This exception will occur if the program isn't
|
||||
# present or isn't executable.
|
||||
debug1('Unable to start firewall manager. Popen failed. '
|
||||
'Command=%r Exception=%s' % (argv, e))
|
||||
continue
|
||||
|
||||
self.argv = argv
|
||||
s1.close()
|
||||
if sys.version_info < (3, 0):
|
||||
# python 2.7
|
||||
self.pfile = s2.makefile('wb+')
|
||||
else:
|
||||
# python 3.5
|
||||
self.pfile = s2.makefile('rwb')
|
||||
if e:
|
||||
log('Spawning firewall manager: %r\n' % self.argv)
|
||||
raise Fatal(e)
|
||||
line = self.pfile.readline()
|
||||
self.check()
|
||||
|
||||
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':
|
||||
raise Fatal('%r expected READY, got %r' % (self.argv, line))
|
||||
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,
|
||||
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
|
||||
user):
|
||||
user, tmark):
|
||||
self.subnets_include = subnets_include
|
||||
self.subnets_exclude = subnets_exclude
|
||||
self.nslist = nslist
|
||||
@ -250,6 +305,7 @@ class FirewallClient:
|
||||
self.dnsport_v4 = dnsport_v4
|
||||
self.udp = udp
|
||||
self.user = user
|
||||
self.tmark = tmark
|
||||
|
||||
def check(self):
|
||||
rv = self.p.poll()
|
||||
@ -260,11 +316,13 @@ class FirewallClient:
|
||||
self.pfile.write(b'ROUTES\n')
|
||||
for (family, ip, width, fport, lport) \
|
||||
in self.subnets_include + self.auto_nets:
|
||||
self.pfile.write(b'%d,%d,0,%s,%d,%d\n'
|
||||
% (family, width, ip.encode("ASCII"), fport, lport))
|
||||
self.pfile.write(b'%d,%d,0,%s,%d,%d\n' % (family, width,
|
||||
ip.encode("ASCII"),
|
||||
fport, lport))
|
||||
for (family, ip, width, fport, lport) in self.subnets_exclude:
|
||||
self.pfile.write(b'%d,%d,1,%s,%d,%d\n'
|
||||
% (family, width, ip.encode("ASCII"), fport, lport))
|
||||
self.pfile.write(b'%d,%d,1,%s,%d,%d\n' % (family, width,
|
||||
ip.encode("ASCII"),
|
||||
fport, lport))
|
||||
|
||||
self.pfile.write(b'NSLIST\n')
|
||||
for (family, ip) in self.nslist:
|
||||
@ -286,7 +344,8 @@ class FirewallClient:
|
||||
else:
|
||||
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()
|
||||
|
||||
line = self.pfile.readline()
|
||||
@ -295,8 +354,8 @@ class FirewallClient:
|
||||
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
|
||||
|
||||
def sethostip(self, hostname, ip):
|
||||
assert(not re.search(b'[^-\w\.]', hostname))
|
||||
assert(not re.search(b'[^0-9.]', ip))
|
||||
assert(not re.search(br'[^-\w\.]', hostname))
|
||||
assert(not re.search(br'[^0-9.]', ip))
|
||||
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
|
||||
self.pfile.flush()
|
||||
|
||||
@ -315,23 +374,23 @@ def expire_connections(now, mux):
|
||||
remove = []
|
||||
for chan, timeout in dnsreqs.items():
|
||||
if timeout < now:
|
||||
debug3('expiring dnsreqs channel=%d\n' % chan)
|
||||
debug3('expiring dnsreqs channel=%d' % chan)
|
||||
remove.append(chan)
|
||||
del mux.channels[chan]
|
||||
for chan in remove:
|
||||
del dnsreqs[chan]
|
||||
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
|
||||
debug3('Remaining DNS requests: %d' % len(dnsreqs))
|
||||
|
||||
remove = []
|
||||
for peer, (chan, timeout) in udp_by_src.items():
|
||||
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'')
|
||||
remove.append(peer)
|
||||
del mux.channels[chan]
|
||||
for peer in remove:
|
||||
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):
|
||||
@ -340,28 +399,28 @@ def onaccept_tcp(listener, method, mux, handlers):
|
||||
sock, srcip = listener.accept()
|
||||
except socket.error as e:
|
||||
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
|
||||
os.close(_extra_fd)
|
||||
try:
|
||||
sock, srcip = listener.accept()
|
||||
sock.close()
|
||||
finally:
|
||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
||||
return
|
||||
else:
|
||||
raise
|
||||
|
||||
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]))
|
||||
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()
|
||||
return
|
||||
chan = mux.next_channel()
|
||||
if not chan:
|
||||
log('warning: too many open channels. Discarded connection.\n')
|
||||
log('warning: too many open channels. Discarded connection.')
|
||||
sock.close()
|
||||
return
|
||||
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
|
||||
@ -374,7 +433,7 @@ def onaccept_tcp(listener, method, mux, handlers):
|
||||
def udp_done(chan, data, method, sock, dstip):
|
||||
(src, srcport, data) = data.split(b",", 2)
|
||||
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)
|
||||
|
||||
|
||||
@ -384,7 +443,7 @@ def onaccept_udp(listener, method, mux, handlers):
|
||||
if t is None:
|
||||
return
|
||||
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:
|
||||
chan, _ = udp_by_src[srcip]
|
||||
else:
|
||||
@ -401,7 +460,7 @@ def onaccept_udp(listener, method, mux, handlers):
|
||||
|
||||
|
||||
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 dnsreqs[chan]
|
||||
method.send_udp(sock, srcip, dstip, data)
|
||||
@ -413,7 +472,13 @@ def ondns(listener, method, mux, handlers):
|
||||
if t is None:
|
||||
return
|
||||
srcip, dstip, data = t
|
||||
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
|
||||
# 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.
|
||||
if dstip is None:
|
||||
debug1('DNS request from %r: %d bytes' % (srcip, len(data)))
|
||||
else:
|
||||
debug1('DNS request from %r to %r: %d bytes' %
|
||||
(srcip, dstip, len(data)))
|
||||
chan = mux.next_channel()
|
||||
dnsreqs[chan] = now + 30
|
||||
mux.send(chan, ssnet.CMD_DNS_REQ, data)
|
||||
@ -423,35 +488,34 @@ def ondns(listener, method, mux, handlers):
|
||||
|
||||
|
||||
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,
|
||||
to_nameserver):
|
||||
|
||||
debug1('Starting client with Python version %s\n'
|
||||
helpers.logprefix = 'c : '
|
||||
debug1('Starting client with Python version %s'
|
||||
% platform.python_version())
|
||||
|
||||
method = fw.method
|
||||
|
||||
handlers = []
|
||||
if helpers.verbose >= 1:
|
||||
helpers.logprefix = 'c : '
|
||||
else:
|
||||
helpers.logprefix = 'client: '
|
||||
debug1('connecting to server...\n')
|
||||
debug1('Connecting to server...')
|
||||
|
||||
try:
|
||||
(serverproc, serversock) = ssh.connect(
|
||||
ssh_cmd, remotename, python,
|
||||
stderr=ssyslog._p and ssyslog._p.stdin,
|
||||
options=dict(latency_control=latency_control,
|
||||
latency_buffer_size=latency_buffer_size,
|
||||
auto_hosts=auto_hosts,
|
||||
to_nameserver=to_nameserver))
|
||||
to_nameserver=to_nameserver,
|
||||
auto_nets=auto_nets))
|
||||
except socket.error as e:
|
||||
if e.args[0] == errno.EPIPE:
|
||||
raise Fatal("failed to establish ssh session (1)")
|
||||
else:
|
||||
raise
|
||||
mux = Mux(serversock, serversock)
|
||||
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
|
||||
handlers.append(mux)
|
||||
|
||||
expected = b'SSHUTTLE0001'
|
||||
@ -470,33 +534,111 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
else:
|
||||
raise
|
||||
|
||||
# Returns None if process is still running (or returns exit code)
|
||||
rv = serverproc.poll()
|
||||
if rv:
|
||||
raise Fatal('server died with error code %d' % rv)
|
||||
if rv is not None:
|
||||
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:
|
||||
raise Fatal('expected server init string %r; got %r'
|
||||
% (expected, initstring))
|
||||
log('Connected.\n')
|
||||
log('Connected to server.')
|
||||
sys.stdout.flush()
|
||||
|
||||
if daemon:
|
||||
daemonize()
|
||||
log('daemonizing (%s).\n' % _pidname)
|
||||
log('daemonizing (%s).' % _pidname)
|
||||
|
||||
def onroutes(routestr):
|
||||
if auto_nets:
|
||||
for line in routestr.strip().split(b'\n'):
|
||||
if not line: continue
|
||||
if not line:
|
||||
continue
|
||||
(family, ip, width) = line.split(b',', 2)
|
||||
family = int(family)
|
||||
width = int(width)
|
||||
ip = ip.decode("ASCII")
|
||||
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:
|
||||
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
|
||||
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
|
||||
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))
|
||||
|
||||
# we definitely want to do this *after* starting ssh, or we might end
|
||||
@ -507,11 +649,16 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
# set --auto-nets, we might as well wait for the message first, then
|
||||
# ignore its contents.
|
||||
mux.got_routes = None
|
||||
fw.start()
|
||||
serverready()
|
||||
|
||||
mux.got_routes = onroutes
|
||||
|
||||
def serverready():
|
||||
fw.start()
|
||||
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
|
||||
|
||||
def onhostlist(hostlist):
|
||||
debug2('got host list: %r\n' % hostlist)
|
||||
debug2('got host list: %r' % hostlist)
|
||||
for line in hostlist.strip().split():
|
||||
if line:
|
||||
name, ip = line.split(b',', 1)
|
||||
@ -527,60 +674,132 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
dns_listener.add_handler(handlers, ondns, method, mux)
|
||||
|
||||
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)))
|
||||
|
||||
while 1:
|
||||
def check_ssh_alive():
|
||||
if daemon:
|
||||
# poll() won't tell us when process exited since the
|
||||
# process is no longer our child (it returns 0 all the
|
||||
# time).
|
||||
try:
|
||||
os.kill(serverproc.pid, 0)
|
||||
except OSError:
|
||||
raise Fatal('ssh connection to server (pid %d) exited.' %
|
||||
serverproc.pid)
|
||||
else:
|
||||
rv = serverproc.poll()
|
||||
if rv:
|
||||
raise Fatal('server died with error code %d' % rv)
|
||||
# poll returns None if process hasn't exited.
|
||||
if rv is not None:
|
||||
raise Fatal('ssh connection to server (pid %d) exited '
|
||||
'with returncode %d' % (serverproc.pid, rv))
|
||||
|
||||
while 1:
|
||||
check_ssh_alive()
|
||||
ssnet.runonce(handlers, mux)
|
||||
if latency_control:
|
||||
mux.check_fullness()
|
||||
|
||||
|
||||
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,
|
||||
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
||||
user):
|
||||
user, sudo_pythonpath, tmark):
|
||||
|
||||
if not remotename:
|
||||
raise Fatal("You must use -r/--remote to specify a remote "
|
||||
"host to route traffic through.")
|
||||
|
||||
if daemon:
|
||||
try:
|
||||
check_daemon(pidfile)
|
||||
except Fatal as e:
|
||||
log("%s\n" % e)
|
||||
log("%s" % e)
|
||||
return 5
|
||||
debug1('Starting sshuttle proxy.\n')
|
||||
debug1('Starting sshuttle proxy (version %s).' % __version__)
|
||||
helpers.logprefix = 'c : '
|
||||
|
||||
fw = FirewallClient(method_name)
|
||||
fw = FirewallClient(method_name, sudo_pythonpath)
|
||||
|
||||
# Get family specific subnet lists
|
||||
# nslist is the list of name severs to intercept. If --dns is
|
||||
# used, we add all DNS servers in resolv.conf. Otherwise, the list
|
||||
# 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.
|
||||
if dns:
|
||||
nslist += resolvconf_nameservers()
|
||||
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:
|
||||
to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
|
||||
else:
|
||||
# option doesn't make sense if we aren't proxying dns
|
||||
else: # if we are not intercepting DNS traffic
|
||||
# ...and the user specified a server to send DNS traffic to.
|
||||
if to_nameserver and len(to_nameserver) > 0:
|
||||
print("WARNING: --to-ns option is ignored unless "
|
||||
"--dns or --ns-hosts is used.")
|
||||
to_nameserver = None
|
||||
|
||||
subnets = subnets_include + subnets_exclude # we don't care here
|
||||
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]
|
||||
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
|
||||
# Get family specific subnet lists. Also, the user may not specify
|
||||
# any subnets if they use --auto-nets. In this case, our subnets
|
||||
# list will be empty and the forwarded subnets will be determined
|
||||
# later by the server.
|
||||
subnets_v4 = [i for i in subnets_include if i[0] == socket.AF_INET]
|
||||
subnets_v6 = [i for i in subnets_include if i[0] == socket.AF_INET6]
|
||||
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
||||
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
||||
|
||||
# Check features available
|
||||
# Get available features from the firewall method
|
||||
avail = fw.method.get_supported_features()
|
||||
|
||||
# A feature is "required" if the user supplies us parameters which
|
||||
# implies that the feature is needed.
|
||||
required = Features()
|
||||
|
||||
# Select the default addresses to bind to / listen to.
|
||||
|
||||
# Assume IPv4 is always available and should always be enabled. If
|
||||
# a method doesn't provide IPv4 support or if we wish to run
|
||||
# ipv6-only, changes to this code are required.
|
||||
assert avail.ipv4
|
||||
required.ipv4 = True
|
||||
|
||||
# listenip_v4 contains user specified value or it is set to "auto".
|
||||
if listenip_v4 == "auto":
|
||||
listenip_v4 = ('127.0.0.1', 0)
|
||||
|
||||
# listenip_v6 is...
|
||||
# None when IPv6 is disabled.
|
||||
# "auto" when listen address is unspecified.
|
||||
# The user specified address if provided by user
|
||||
if listenip_v6 is None:
|
||||
debug1("IPv6 disabled by --disable-ipv6")
|
||||
if listenip_v6 == "auto":
|
||||
if avail.ipv6:
|
||||
debug1("IPv6 enabled: Using default IPv6 listen address ::1")
|
||||
listenip_v6 = ('::1', 0)
|
||||
else:
|
||||
debug1("IPv6 disabled since it isn't supported by method "
|
||||
"%s." % fw.method.name)
|
||||
listenip_v6 = None
|
||||
|
||||
# Make final decision about enabling IPv6:
|
||||
required.ipv6 = False
|
||||
if listenip_v6:
|
||||
required.ipv6 = True
|
||||
|
||||
# If we get here, it is possible that listenip_v6 was user
|
||||
# specified but not supported by the current method.
|
||||
if required.ipv6 and not avail.ipv6:
|
||||
raise Fatal("An IPv6 listen address was supplied, but IPv6 is "
|
||||
"disabled at your request or is unsupported by the %s "
|
||||
"method." % fw.method.name)
|
||||
|
||||
if user is not None:
|
||||
if getpwnam is None:
|
||||
raise Fatal("Routing by user not available on this system.")
|
||||
@ -588,33 +807,66 @@ def main(listenip_v6, listenip_v4,
|
||||
user = getpwnam(user).pw_uid
|
||||
except KeyError:
|
||||
raise Fatal("User %s does not exist." % user)
|
||||
|
||||
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
|
||||
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
|
||||
required.udp = avail.udp
|
||||
required.dns = len(nslist) > 0
|
||||
required.user = False if user is None else True
|
||||
|
||||
# if IPv6 not supported, ignore IPv6 DNS servers
|
||||
if not required.ipv6:
|
||||
if not required.ipv6 and len(subnets_v6) > 0:
|
||||
print("WARNING: IPv6 subnets were ignored because IPv6 is disabled "
|
||||
"in sshuttle.")
|
||||
subnets_v6 = []
|
||||
subnets_include = subnets_v4
|
||||
|
||||
required.udp = avail.udp # automatically enable UDP if it is available
|
||||
required.dns = len(nslist) > 0
|
||||
|
||||
# Remove DNS servers using IPv6.
|
||||
if required.dns:
|
||||
if not required.ipv6 and len(nslist_v6) > 0:
|
||||
print("WARNING: Your system is configured to use an IPv6 DNS "
|
||||
"server but sshuttle is not using IPv6. Therefore DNS "
|
||||
"traffic your system sends to the IPv6 DNS server won't "
|
||||
"be redirected via sshuttle to the remote machine.")
|
||||
nslist_v6 = []
|
||||
nslist = nslist_v4
|
||||
|
||||
if len(nslist) == 0:
|
||||
raise Fatal("Can't redirect DNS traffic since IPv6 is not "
|
||||
"enabled in sshuttle and all of the system DNS "
|
||||
"servers are IPv6.")
|
||||
|
||||
# If we aren't using IPv6, we can safely ignore excluded IPv6 subnets.
|
||||
if not required.ipv6:
|
||||
orig_len = len(subnets_exclude)
|
||||
subnets_exclude = [i for i in subnets_exclude
|
||||
if i[0] == socket.AF_INET]
|
||||
if len(subnets_exclude) < orig_len:
|
||||
print("WARNING: Ignoring one or more excluded IPv6 subnets "
|
||||
"because IPv6 is not enabled.")
|
||||
|
||||
# This will print error messages if we required a feature that
|
||||
# isn't available by the current method.
|
||||
fw.method.assert_features(required)
|
||||
|
||||
if required.ipv6 and listenip_v6 is None:
|
||||
raise Fatal("IPv6 required but not listening.")
|
||||
|
||||
# display features enabled
|
||||
debug1("IPv6 enabled: %r\n" % required.ipv6)
|
||||
debug1("UDP enabled: %r\n" % required.udp)
|
||||
debug1("DNS enabled: %r\n" % required.dns)
|
||||
debug1("User enabled: %r\n" % required.user)
|
||||
def feature_status(label, enabled, available):
|
||||
msg = label + ": "
|
||||
if enabled:
|
||||
msg += "on"
|
||||
else:
|
||||
msg += "off "
|
||||
if available:
|
||||
msg += "(available)"
|
||||
else:
|
||||
msg += "(not available with %s method)" % fw.method.name
|
||||
debug1(msg)
|
||||
|
||||
# bind to required ports
|
||||
if listenip_v4 == "auto":
|
||||
listenip_v4 = ('127.0.0.1', 0)
|
||||
debug1("Method: %s" % fw.method.name)
|
||||
feature_status("IPv4", required.ipv4, avail.ipv4)
|
||||
feature_status("IPv6", required.ipv6, avail.ipv6)
|
||||
feature_status("UDP ", required.udp, avail.udp)
|
||||
feature_status("DNS ", required.dns, avail.dns)
|
||||
feature_status("User", required.user, avail.user)
|
||||
|
||||
# Exclude traffic destined to our listen addresses.
|
||||
if required.ipv4 and \
|
||||
not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
|
||||
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0))
|
||||
@ -623,6 +875,25 @@ def main(listenip_v6, listenip_v4,
|
||||
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
|
||||
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
|
||||
|
||||
# We don't print the IP+port of where we are listening here
|
||||
# because we do that below when we have identified the ports to
|
||||
# listen on.
|
||||
debug1("Subnets to forward through remote host (type, IP, cidr mask "
|
||||
"width, startPort, endPort):")
|
||||
for i in subnets_include:
|
||||
debug1(" "+str(i))
|
||||
if auto_nets:
|
||||
debug1("NOTE: Additional subnets to forward may be added below by "
|
||||
"--auto-nets.")
|
||||
debug1("Subnets to exclude from forwarding:")
|
||||
for i in subnets_exclude:
|
||||
debug1(" "+str(i))
|
||||
if required.dns:
|
||||
debug1("DNS requests normally directed at these servers will be "
|
||||
"redirected to remote:")
|
||||
for i in nslist:
|
||||
debug1(" "+str(i))
|
||||
|
||||
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
|
||||
ports = [0, ]
|
||||
@ -638,9 +909,8 @@ def main(listenip_v6, listenip_v4,
|
||||
redirectport_v6 = 0
|
||||
redirectport_v4 = 0
|
||||
bound = False
|
||||
debug2('Binding redirector:')
|
||||
for port in ports:
|
||||
debug2(' %d' % port)
|
||||
debug2('Trying to bind redirector on port %d' % port)
|
||||
tcp_listener = MultiListener()
|
||||
|
||||
if required.udp:
|
||||
@ -682,7 +952,6 @@ def main(listenip_v6, listenip_v4,
|
||||
else:
|
||||
raise e
|
||||
|
||||
debug2('\n')
|
||||
if not bound:
|
||||
assert(last_e)
|
||||
raise last_e
|
||||
@ -694,11 +963,11 @@ def main(listenip_v6, listenip_v4,
|
||||
bound = False
|
||||
if required.dns:
|
||||
# search for spare port for DNS
|
||||
debug2('Binding DNS:')
|
||||
ports = range(12300, 9000, -1)
|
||||
for port in ports:
|
||||
debug2(' %d' % port)
|
||||
if port in used_ports: continue
|
||||
debug2('Trying to bind DNS redirector on port %d' % port)
|
||||
if port in used_ports:
|
||||
continue
|
||||
|
||||
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
||||
|
||||
@ -727,7 +996,7 @@ def main(listenip_v6, listenip_v4,
|
||||
used_ports.append(port)
|
||||
else:
|
||||
raise e
|
||||
debug2('\n')
|
||||
|
||||
dns_listener.print_listening("DNS")
|
||||
if not bound:
|
||||
assert(last_e)
|
||||
@ -769,19 +1038,22 @@ def main(listenip_v6, listenip_v4,
|
||||
# start the firewall
|
||||
fw.setup(subnets_include, subnets_exclude, nslist,
|
||||
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
|
||||
required.udp, user)
|
||||
required.udp, user, tmark)
|
||||
|
||||
# start the client process
|
||||
try:
|
||||
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||
python, latency_control, dns_listener,
|
||||
seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver)
|
||||
python, latency_control, latency_buffer_size,
|
||||
dns_listener, seed_hosts, auto_hosts, auto_nets,
|
||||
daemon, to_nameserver)
|
||||
finally:
|
||||
try:
|
||||
if daemon:
|
||||
# it's not our child anymore; can't waitpid
|
||||
fw.p.returncode = 0
|
||||
fw.done()
|
||||
sdnotify.send(sdnotify.stop())
|
||||
|
||||
finally:
|
||||
if daemon:
|
||||
daemon_cleanup()
|
||||
|
@ -1,5 +1,6 @@
|
||||
import re
|
||||
import socket
|
||||
import platform
|
||||
import sshuttle.helpers as helpers
|
||||
import sshuttle.client as client
|
||||
import sshuttle.firewall as firewall
|
||||
@ -7,16 +8,35 @@ import sshuttle.hostwatch as hostwatch
|
||||
import sshuttle.ssyslog as ssyslog
|
||||
from sshuttle.options import parser, parse_ipport
|
||||
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
||||
from sshuttle.sudoers import sudoers
|
||||
|
||||
|
||||
def main():
|
||||
opt = parser.parse_args()
|
||||
|
||||
if opt.sudoers or opt.sudoers_no_modify:
|
||||
if platform.platform().startswith('OpenBSD'):
|
||||
log('Automatic sudoers does not work on BSD')
|
||||
return 1
|
||||
|
||||
if not opt.sudoers_filename:
|
||||
log('--sudoers-file must be set or omitted.')
|
||||
return 1
|
||||
|
||||
sudoers(
|
||||
user_name=opt.sudoers_user,
|
||||
no_modify=opt.sudoers_no_modify,
|
||||
file_name=opt.sudoers_filename
|
||||
)
|
||||
|
||||
if opt.daemon:
|
||||
opt.syslog = 1
|
||||
if opt.wrap:
|
||||
import sshuttle.ssnet as ssnet
|
||||
ssnet.MAX_CHANNEL = opt.wrap
|
||||
if opt.latency_buffer_size:
|
||||
import sshuttle.ssnet as ssnet
|
||||
ssnet.LATENCY_BUFFER_SIZE = opt.latency_buffer_size
|
||||
helpers.verbose = opt.verbose
|
||||
|
||||
try:
|
||||
@ -27,8 +47,16 @@ def main():
|
||||
elif opt.hostwatch:
|
||||
return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
|
||||
else:
|
||||
includes = opt.subnets + opt.subnets_file
|
||||
excludes = opt.exclude
|
||||
# parse_subnetports() is used to create a list of includes
|
||||
# and excludes. It is called once for each parameter and
|
||||
# returns a list of one or more items for each subnet (it
|
||||
# can return more than one item when a hostname in the
|
||||
# parameter resolves to multiple IP addresses. Here, we
|
||||
# flatten these lists.
|
||||
includes = [item for sublist in opt.subnets+opt.subnets_file
|
||||
for item in sublist]
|
||||
excludes = [item for sublist in opt.exclude for item in sublist]
|
||||
|
||||
if not includes and not opt.auto_nets:
|
||||
parser.error('at least one subnet, subnet file, '
|
||||
'or -N expected')
|
||||
@ -57,14 +85,24 @@ def main():
|
||||
ipport_v4 = "auto"
|
||||
# parse_ipport6('[::1]:0')
|
||||
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:
|
||||
ssyslog.start_syslog()
|
||||
ssyslog.close_stdin()
|
||||
ssyslog.stdout_to_syslog()
|
||||
ssyslog.stderr_to_syslog()
|
||||
return_code = client.main(ipport_v6, ipport_v4,
|
||||
opt.ssh_cmd,
|
||||
remotename,
|
||||
opt.python,
|
||||
opt.latency_control,
|
||||
opt.latency_buffer_size,
|
||||
opt.dns,
|
||||
nslist,
|
||||
opt.method,
|
||||
@ -76,18 +114,20 @@ def main():
|
||||
opt.daemon,
|
||||
opt.to_ns,
|
||||
opt.pidfile,
|
||||
opt.user)
|
||||
opt.user,
|
||||
opt.sudo_pythonpath,
|
||||
opt.tmark)
|
||||
|
||||
if return_code == 0:
|
||||
log('Normal exit code, exiting...')
|
||||
else:
|
||||
log('Abnormal exit code detected, failing...' % return_code)
|
||||
log('Abnormal exit code %d detected, failing...' % return_code)
|
||||
return return_code
|
||||
|
||||
except Fatal as e:
|
||||
log('fatal: %s\n' % e)
|
||||
log('fatal: %s' % e)
|
||||
return 99
|
||||
except KeyboardInterrupt:
|
||||
log('\n')
|
||||
log('Keyboard interrupt: exiting.\n')
|
||||
log('Keyboard interrupt: exiting.')
|
||||
return 1
|
||||
|
@ -1,16 +1,19 @@
|
||||
import errno
|
||||
import socket
|
||||
import signal
|
||||
import sshuttle.ssyslog as ssyslog
|
||||
import sshuttle.sdnotify as sdnotify
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import traceback
|
||||
from sshuttle.helpers import debug1, debug2, Fatal
|
||||
import subprocess as ssubprocess
|
||||
|
||||
import sshuttle.ssyslog as ssyslog
|
||||
import sshuttle.helpers as helpers
|
||||
from sshuttle.helpers import log, debug1, debug2, Fatal
|
||||
from sshuttle.methods import get_auto_method, get_method
|
||||
|
||||
HOSTSFILE = '/etc/hosts'
|
||||
sshuttle_pid = None
|
||||
|
||||
|
||||
def rewrite_etc_hosts(hostmap, port):
|
||||
@ -47,25 +50,54 @@ def rewrite_etc_hosts(hostmap, port):
|
||||
os.rename(tmpname, HOSTSFILE)
|
||||
|
||||
|
||||
def restore_etc_hosts(port):
|
||||
def restore_etc_hosts(hostmap, port):
|
||||
# Only restore if we added hosts to /etc/hosts previously.
|
||||
if len(hostmap) > 0:
|
||||
debug2('undoing /etc/hosts changes.')
|
||||
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
|
||||
def setup_daemon():
|
||||
if os.getuid() != 0:
|
||||
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
||||
raise Fatal('You must be root (or enable su/sudo) to set the firewall')
|
||||
|
||||
# don't disappear if our controlling terminal or stdout/stderr
|
||||
# disappears; we still have to clean up.
|
||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTERM, firewall_exit)
|
||||
signal.signal(signal.SIGINT, firewall_exit)
|
||||
|
||||
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
||||
# I'll die automatically.
|
||||
# Calling setsid() here isn't strictly necessary. However, it forces
|
||||
# Ctrl+C to get sent to the main sshuttle process instead of to
|
||||
# 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
|
||||
# are both attached to stdout initially. Clone stdout into stdin so we
|
||||
@ -85,19 +117,50 @@ def subnet_weight(s):
|
||||
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
|
||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||
# otherwise we delete it, then make them from scratch.
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
def main(method_name, syslog):
|
||||
helpers.logprefix = 'fw: '
|
||||
stdin, stdout = setup_daemon()
|
||||
hostmap = {}
|
||||
|
||||
debug1('firewall manager: Starting firewall with Python version %s\n'
|
||||
debug1('Starting firewall with Python version %s'
|
||||
% platform.python_version())
|
||||
|
||||
if method_name == "auto":
|
||||
@ -109,7 +172,12 @@ def main(method_name, syslog):
|
||||
ssyslog.start_syslog()
|
||||
ssyslog.stderr_to_syslog()
|
||||
|
||||
debug1('firewall manager: ready method name %s.\n' % method.name)
|
||||
if not method.is_supported():
|
||||
raise Fatal("The %s method is not supported on this machine. "
|
||||
"Check that the appropriate programs are in your "
|
||||
"PATH." % method_name)
|
||||
|
||||
debug1('ready method name %s.' % method.name)
|
||||
stdout.write('READY %s\n' % method.name)
|
||||
stdout.flush()
|
||||
|
||||
@ -122,18 +190,18 @@ def main(method_name, syslog):
|
||||
|
||||
subnets = []
|
||||
if line != 'ROUTES\n':
|
||||
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
||||
raise Fatal('expected ROUTES but got %r' % line)
|
||||
while 1:
|
||||
line = stdin.readline(128)
|
||||
if not line:
|
||||
raise Fatal('firewall: expected route but got %r' % line)
|
||||
raise Fatal('expected route but got %r' % line)
|
||||
elif line.startswith("NSLIST\n"):
|
||||
break
|
||||
try:
|
||||
(family, width, exclude, ip, fport, lport) = \
|
||||
line.strip().split(',', 5)
|
||||
except:
|
||||
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
|
||||
except BaseException:
|
||||
raise Fatal('expected route or NSLIST but got %r' % line)
|
||||
subnets.append((
|
||||
int(family),
|
||||
int(width),
|
||||
@ -141,31 +209,31 @@ def main(method_name, syslog):
|
||||
ip,
|
||||
int(fport),
|
||||
int(lport)))
|
||||
debug2('firewall manager: Got subnets: %r\n' % subnets)
|
||||
debug2('Got subnets: %r' % subnets)
|
||||
|
||||
nslist = []
|
||||
if line != 'NSLIST\n':
|
||||
raise Fatal('firewall: expected NSLIST but got %r' % line)
|
||||
raise Fatal('expected NSLIST but got %r' % line)
|
||||
while 1:
|
||||
line = stdin.readline(128)
|
||||
if not line:
|
||||
raise Fatal('firewall: expected nslist but got %r' % line)
|
||||
raise Fatal('expected nslist but got %r' % line)
|
||||
elif line.startswith("PORTS "):
|
||||
break
|
||||
try:
|
||||
(family, ip) = line.strip().split(',', 1)
|
||||
except:
|
||||
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
|
||||
except BaseException:
|
||||
raise Fatal('expected nslist or PORTS but got %r' % line)
|
||||
nslist.append((int(family), ip))
|
||||
debug2('firewall manager: Got partial nslist: %r\n' % nslist)
|
||||
debug2('firewall manager: Got nslist: %r\n' % nslist)
|
||||
debug2('Got partial nslist: %r' % nslist)
|
||||
debug2('Got nslist: %r' % nslist)
|
||||
|
||||
if not line.startswith('PORTS '):
|
||||
raise Fatal('firewall: expected PORTS but got %r' % line)
|
||||
raise Fatal('expected PORTS but got %r' % line)
|
||||
_, _, ports = line.partition(" ")
|
||||
ports = ports.split(",")
|
||||
if len(ports) != 4:
|
||||
raise Fatal('firewall: expected 4 ports but got %d' % len(ports))
|
||||
raise Fatal('expected 4 ports but got %d' % len(ports))
|
||||
port_v6 = int(ports[0])
|
||||
port_v4 = int(ports[1])
|
||||
dnsport_v6 = int(ports[2])
|
||||
@ -180,21 +248,24 @@ def main(method_name, syslog):
|
||||
assert(dnsport_v4 >= 0)
|
||||
assert(dnsport_v4 <= 65535)
|
||||
|
||||
debug2('firewall manager: Got ports: %d,%d,%d,%d\n'
|
||||
debug2('Got ports: %d,%d,%d,%d'
|
||||
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
||||
|
||||
line = stdin.readline(128)
|
||||
if not line:
|
||||
raise Fatal('firewall: expected GO but got %r' % line)
|
||||
raise Fatal('expected GO but got %r' % line)
|
||||
elif not line.startswith("GO "):
|
||||
raise Fatal('firewall: expected GO but got %r' % line)
|
||||
raise Fatal('expected GO but got %r' % line)
|
||||
|
||||
_, _, 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))
|
||||
sshuttle_pid = int(sshuttle_pid)
|
||||
if user == '-':
|
||||
user = None
|
||||
debug2('firewall manager: 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]
|
||||
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
||||
@ -202,25 +273,24 @@ def main(method_name, syslog):
|
||||
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
||||
|
||||
try:
|
||||
debug1('firewall manager: setting up.\n')
|
||||
debug1('setting up.')
|
||||
|
||||
if subnets_v6 or nslist_v6:
|
||||
debug2('firewall manager: setting up IPv6.\n')
|
||||
debug2('setting up IPv6.')
|
||||
method.setup_firewall(
|
||||
port_v6, dnsport_v6, nslist_v6,
|
||||
socket.AF_INET6, subnets_v6, udp,
|
||||
user)
|
||||
user, tmark)
|
||||
|
||||
if subnets_v4 or nslist_v4:
|
||||
debug2('firewall manager: setting up IPv4.\n')
|
||||
debug2('setting up IPv4.')
|
||||
method.setup_firewall(
|
||||
port_v4, dnsport_v4, nslist_v4,
|
||||
socket.AF_INET, subnets_v4, udp,
|
||||
user)
|
||||
user, tmark)
|
||||
|
||||
flush_systemd_dns_cache()
|
||||
stdout.write('STARTED\n')
|
||||
sdnotify.send(sdnotify.ready(),
|
||||
sdnotify.status('Connected'))
|
||||
|
||||
try:
|
||||
stdout.flush()
|
||||
@ -237,54 +307,56 @@ def main(method_name, syslog):
|
||||
if line.startswith('HOST '):
|
||||
(name, ip) = line[5:].strip().split(',', 1)
|
||||
hostmap[name] = ip
|
||||
debug2('firewall manager: setting up /etc/hosts.\n')
|
||||
debug2('setting up /etc/hosts.')
|
||||
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
|
||||
elif line:
|
||||
if not method.firewall_command(line):
|
||||
raise Fatal('firewall: expected command, got %r' % line)
|
||||
raise Fatal('expected command, got %r' % line)
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
sdnotify.send(sdnotify.stop())
|
||||
debug1('firewall manager: undoing changes.\n')
|
||||
except:
|
||||
pass
|
||||
debug1('undoing changes.')
|
||||
except BaseException:
|
||||
debug2('An error occurred, ignoring it.')
|
||||
|
||||
try:
|
||||
if subnets_v6 or nslist_v6:
|
||||
debug2('firewall manager: undoing IPv6 changes.\n')
|
||||
debug2('undoing IPv6 changes.')
|
||||
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
|
||||
except:
|
||||
except BaseException:
|
||||
try:
|
||||
debug1("firewall manager: "
|
||||
"Error trying to undo IPv6 firewall.\n")
|
||||
for line in traceback.format_exc().splitlines():
|
||||
debug1("---> %s\n" % line)
|
||||
except:
|
||||
pass
|
||||
debug1("Error trying to undo IPv6 firewall.")
|
||||
debug1(traceback.format_exc())
|
||||
except BaseException:
|
||||
debug2('An error occurred, ignoring it.')
|
||||
|
||||
try:
|
||||
if subnets_v4 or nslist_v4:
|
||||
debug2('firewall manager: undoing IPv4 changes.\n')
|
||||
debug2('undoing IPv4 changes.')
|
||||
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
|
||||
except:
|
||||
except BaseException:
|
||||
try:
|
||||
debug1("firewall manager: "
|
||||
"Error trying to undo IPv4 firewall.\n")
|
||||
for line in traceback.format_exc().splitlines():
|
||||
debug1("firewall manager: ---> %s\n" % line)
|
||||
except:
|
||||
pass
|
||||
debug1("Error trying to undo IPv4 firewall.")
|
||||
debug1(traceback.format_exc())
|
||||
except BaseException:
|
||||
debug2('An error occurred, ignoring it.')
|
||||
|
||||
try:
|
||||
debug2('firewall manager: undoing /etc/hosts changes.\n')
|
||||
restore_etc_hosts(port_v6 or port_v4)
|
||||
except:
|
||||
# debug2() message printed in restore_etc_hosts() function.
|
||||
restore_etc_hosts(hostmap, port_v6 or port_v4)
|
||||
except BaseException:
|
||||
try:
|
||||
debug1("firewall manager: "
|
||||
"Error trying to undo /etc/hosts changes.\n")
|
||||
for line in traceback.format_exc().splitlines():
|
||||
debug1("firewall manager: ---> %s\n" % line)
|
||||
except:
|
||||
pass
|
||||
debug1("Error trying to undo /etc/hosts changes.")
|
||||
debug1(traceback.format_exc())
|
||||
except BaseException:
|
||||
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.")
|
||||
|
@ -1,34 +1,36 @@
|
||||
import sys
|
||||
import socket
|
||||
import errno
|
||||
import os
|
||||
|
||||
logprefix = ''
|
||||
verbose = 0
|
||||
|
||||
if sys.version_info[0] == 3:
|
||||
binary_type = bytes
|
||||
|
||||
def b(s):
|
||||
def b(s):
|
||||
return s.encode("ASCII")
|
||||
else:
|
||||
binary_type = str
|
||||
|
||||
def b(s):
|
||||
return s
|
||||
|
||||
|
||||
def log(s):
|
||||
global logprefix
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
if s.find("\n") != -1:
|
||||
# Put newline at end of string if line doesn't have one.
|
||||
if not s.endswith("\n"):
|
||||
s = s+"\n"
|
||||
|
||||
prefix = logprefix
|
||||
s = s.rstrip("\n")
|
||||
for line in s.split("\n"):
|
||||
sys.stderr.write(prefix + line + "\n")
|
||||
prefix = "---> "
|
||||
else:
|
||||
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()
|
||||
except IOError:
|
||||
# this could happen if stderr gets forcibly disconnected, eg. because
|
||||
@ -55,23 +57,70 @@ class Fatal(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def resolvconf_nameservers():
|
||||
l = []
|
||||
for line in open('/etc/resolv.conf'):
|
||||
def resolvconf_nameservers(systemd_resolved):
|
||||
"""Retrieves a list of tuples (address type, address as a string) of
|
||||
the DNS servers used by the system to resolve hostnames.
|
||||
|
||||
If parameter is False, DNS servers are retrieved from only
|
||||
/etc/resolv.conf. This behavior makes sense for the sshuttle
|
||||
server.
|
||||
|
||||
If parameter is True, we retrieve information from both
|
||||
/etc/resolv.conf and /run/systemd/resolve/resolv.conf (if it
|
||||
exists). This behavior makes sense for the sshuttle client.
|
||||
|
||||
"""
|
||||
|
||||
# Historically, we just needed to read /etc/resolv.conf.
|
||||
#
|
||||
# If systemd-resolved is active, /etc/resolv.conf will point to
|
||||
# localhost and the actual DNS servers that systemd-resolved uses
|
||||
# are stored in /run/systemd/resolve/resolv.conf. For programs
|
||||
# that use the localhost DNS server, having sshuttle read
|
||||
# /etc/resolv.conf is sufficient. However, resolved provides other
|
||||
# ways of resolving hostnames (such as via dbus) that may not
|
||||
# route requests through localhost. So, we retrieve a list of DNS
|
||||
# servers that resolved uses so we can intercept those as well.
|
||||
#
|
||||
# For more information about systemd-resolved, see:
|
||||
# https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html
|
||||
#
|
||||
# On machines without systemd-resolved, we expect opening the
|
||||
# second file will fail.
|
||||
files = ['/etc/resolv.conf']
|
||||
if systemd_resolved:
|
||||
files += ['/run/systemd/resolve/resolv.conf']
|
||||
|
||||
nsservers = []
|
||||
for f in files:
|
||||
this_file_nsservers = []
|
||||
try:
|
||||
for line in open(f):
|
||||
words = line.lower().split()
|
||||
if len(words) >= 2 and words[0] == 'nameserver':
|
||||
l.append(family_ip_tuple(words[1]))
|
||||
return l
|
||||
this_file_nsservers.append(family_ip_tuple(words[1]))
|
||||
debug2("Found DNS servers in %s: %s" %
|
||||
(f, [n[1] for n in this_file_nsservers]))
|
||||
nsservers += this_file_nsservers
|
||||
except OSError as e:
|
||||
debug3("Failed to read %s when looking for DNS servers: %s" %
|
||||
(f, e.strerror))
|
||||
|
||||
return nsservers
|
||||
|
||||
|
||||
def resolvconf_random_nameserver():
|
||||
l = resolvconf_nameservers()
|
||||
if l:
|
||||
if len(l) > 1:
|
||||
def resolvconf_random_nameserver(systemd_resolved):
|
||||
"""Return a random nameserver selected from servers produced by
|
||||
resolvconf_nameservers(). See documentation for
|
||||
resolvconf_nameservers() for a description of the parameter.
|
||||
"""
|
||||
lines = resolvconf_nameservers(systemd_resolved)
|
||||
if lines:
|
||||
if len(lines) > 1:
|
||||
# don't import this unless we really need it
|
||||
import random
|
||||
random.shuffle(l)
|
||||
return l[0]
|
||||
random.shuffle(lines)
|
||||
return lines[0]
|
||||
else:
|
||||
return (socket.AF_INET, '127.0.0.1')
|
||||
|
||||
@ -106,3 +155,75 @@ def family_to_string(family):
|
||||
return "AF_INET"
|
||||
else:
|
||||
return str(family)
|
||||
|
||||
|
||||
def get_env():
|
||||
"""An environment for sshuttle subprocesses. See get_path()."""
|
||||
env = {
|
||||
'PATH': get_path(),
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
return env
|
||||
|
||||
|
||||
def get_path():
|
||||
"""Returns a string of paths separated by os.pathsep.
|
||||
|
||||
Users might not have all of the programs sshuttle needs in their
|
||||
PATH variable (i.e., some programs might be in /sbin). Use PATH
|
||||
and a hardcoded set of paths to search through. This function is
|
||||
used by our which() and get_env() functions. If which() and the
|
||||
subprocess environments differ, programs that which() finds might
|
||||
not be found at run time (or vice versa).
|
||||
"""
|
||||
path = []
|
||||
if "PATH" in os.environ:
|
||||
path += os.environ["PATH"].split(os.pathsep)
|
||||
# Python default paths.
|
||||
path += os.defpath.split(os.pathsep)
|
||||
# /sbin, etc are not in os.defpath and may not be in PATH either.
|
||||
# /bin/ and /usr/bin below are probably redundant.
|
||||
path += ['/bin', '/usr/bin', '/sbin', '/usr/sbin']
|
||||
|
||||
# Remove duplicates. Not strictly necessary.
|
||||
path_dedup = []
|
||||
for i in path:
|
||||
if i not in path_dedup:
|
||||
path_dedup.append(i)
|
||||
|
||||
return os.pathsep.join(path_dedup)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 3):
|
||||
from shutil import which as _which
|
||||
else:
|
||||
# Although sshuttle does not officially support older versions of
|
||||
# Python, some still run the sshuttle server on remote machines
|
||||
# with old versions of python.
|
||||
def _which(file, mode=os.F_OK | os.X_OK, path=None):
|
||||
if path is not None:
|
||||
search_paths = path.split(os.pathsep)
|
||||
elif "PATH" in os.environ:
|
||||
search_paths = os.environ["PATH"].split(os.pathsep)
|
||||
else:
|
||||
search_paths = os.defpath.split(os.pathsep)
|
||||
|
||||
for p in search_paths:
|
||||
filepath = os.path.join(p, file)
|
||||
if os.path.exists(filepath) and os.access(filepath, mode):
|
||||
return filepath
|
||||
return None
|
||||
|
||||
|
||||
def which(file, mode=os.F_OK | os.X_OK):
|
||||
"""A wrapper around shutil.which() that searches a predictable set of
|
||||
paths and is more verbose about what is happening. See get_path()
|
||||
for more information.
|
||||
"""
|
||||
path = get_path()
|
||||
rv = _which(file, mode, path)
|
||||
if rv:
|
||||
debug2("which() found '%s' at %s" % (file, rv))
|
||||
else:
|
||||
debug2("which() could not find '%s' in %s" % (file, path))
|
||||
return rv
|
||||
|
@ -9,22 +9,22 @@ import platform
|
||||
|
||||
import subprocess as ssubprocess
|
||||
import sshuttle.helpers as helpers
|
||||
from sshuttle.helpers import log, debug1, debug2, debug3
|
||||
from sshuttle.helpers import log, debug1, debug2, debug3, get_env
|
||||
|
||||
POLL_TIME = 60 * 15
|
||||
NETSTAT_POLL_TIME = 30
|
||||
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
|
||||
|
||||
# Have we already failed to write CACHEFILE?
|
||||
CACHE_WRITE_FAILED = False
|
||||
|
||||
_nmb_ok = True
|
||||
_smb_ok = True
|
||||
hostnames = {}
|
||||
queue = {}
|
||||
try:
|
||||
null = open('/dev/null', 'wb')
|
||||
null = open(os.devnull, 'wb')
|
||||
except IOError:
|
||||
_, e = sys.exc_info()[:2]
|
||||
log('warning: %s\n' % e)
|
||||
log('warning: %s' % e)
|
||||
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
||||
|
||||
|
||||
@ -33,7 +33,10 @@ def _is_ip(s):
|
||||
|
||||
|
||||
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())
|
||||
global CACHE_WRITE_FAILED
|
||||
try:
|
||||
f = open(tmpname, 'wb')
|
||||
for name, ip in sorted(hostnames.items()):
|
||||
@ -41,33 +44,50 @@ def write_host_cache():
|
||||
f.close()
|
||||
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
|
||||
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:
|
||||
os.unlink(tmpname)
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
def read_host_cache():
|
||||
"""If possible, read the cache file from disk to populate hosts that
|
||||
were found in a previous sshuttle run."""
|
||||
try:
|
||||
f = open(CACHEFILE)
|
||||
except IOError:
|
||||
except (OSError, IOError):
|
||||
_, e = sys.exc_info()[:2]
|
||||
if e.errno == errno.ENOENT:
|
||||
return
|
||||
else:
|
||||
raise
|
||||
log("Failed to read existing host cache file %s on remote host"
|
||||
% CACHEFILE)
|
||||
return
|
||||
for line in f:
|
||||
words = line.strip().split(',')
|
||||
if len(words) == 2:
|
||||
(name, ip) = words
|
||||
name = re.sub(r'[^-\w\.]', '-', name).strip()
|
||||
# Remove characters that shouldn't be in IP
|
||||
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
||||
if name and ip:
|
||||
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'[^-\w\.]', '_', hostname)
|
||||
if (ip.startswith('127.') or ip.startswith('255.') or
|
||||
@ -80,168 +100,87 @@ def found_host(name, ip):
|
||||
oldip = hostnames.get(name)
|
||||
if oldip != 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))
|
||||
write_host_cache()
|
||||
|
||||
|
||||
def _check_etc_hosts():
|
||||
debug2(' > hosts\n')
|
||||
for line in open('/etc/hosts'):
|
||||
line = re.sub(r'#.*', '', line)
|
||||
"""If possible, read /etc/hosts to find hosts."""
|
||||
filename = '/etc/hosts'
|
||||
debug2(' > Reading %s on remote host' % filename)
|
||||
try:
|
||||
for line in open(filename):
|
||||
line = re.sub(r'#.*', '', line) # remove comments
|
||||
words = line.strip().split()
|
||||
if not words:
|
||||
continue
|
||||
ip = words[0]
|
||||
names = words[1:]
|
||||
if _is_ip(ip):
|
||||
debug3('< %s %r\n' % (ip, names))
|
||||
names = words[1:]
|
||||
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):
|
||||
debug2(' > rev: %s\n' % ip)
|
||||
"""Use reverse DNS to try to get hostnames from an IP addresses."""
|
||||
debug2(' > rev: %s' % ip)
|
||||
try:
|
||||
r = socket.gethostbyaddr(ip)
|
||||
debug3('< %s\n' % r[0])
|
||||
debug3('< %s' % r[0])
|
||||
check_host(r[0])
|
||||
found_host(r[0], ip)
|
||||
except socket.herror:
|
||||
except (OSError, socket.error, UnicodeError):
|
||||
# This case is expected to occur regularly.
|
||||
# debug3('< %s gethostbyaddr failed on remote host' % ip)
|
||||
pass
|
||||
|
||||
|
||||
def _check_dns(hostname):
|
||||
debug2(' > dns: %s\n' % hostname)
|
||||
debug2(' > dns: %s' % hostname)
|
||||
try:
|
||||
ip = socket.gethostbyname(hostname)
|
||||
debug3('< %s\n' % ip)
|
||||
debug3('< %s' % ip)
|
||||
check_host(ip)
|
||||
found_host(hostname, ip)
|
||||
except socket.gaierror:
|
||||
except (socket.gaierror, UnicodeError):
|
||||
pass
|
||||
|
||||
|
||||
def _check_netstat():
|
||||
debug2(' > netstat\n')
|
||||
debug2(' > netstat')
|
||||
argv = ['netstat', '-n']
|
||||
try:
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||
env=get_env())
|
||||
content = p.stdout.read().decode("ASCII")
|
||||
p.wait()
|
||||
except OSError:
|
||||
_, e = sys.exc_info()[:2]
|
||||
log('%r failed: %r\n' % (argv, e))
|
||||
log('%r failed: %r' % (argv, e))
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
def _check_smb(hostname):
|
||||
return
|
||||
global _smb_ok
|
||||
if not _smb_ok:
|
||||
return
|
||||
argv = ['smbclient', '-U', '%', '-L', hostname]
|
||||
debug2(' > smb: %s\n' % hostname)
|
||||
try:
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||
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
|
||||
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
|
||||
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
|
||||
try:
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||
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):
|
||||
if _is_ip(hostname):
|
||||
_enqueue(_check_revdns, hostname)
|
||||
else:
|
||||
_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):
|
||||
@ -260,12 +199,9 @@ def _stdin_still_ok(timeout):
|
||||
|
||||
|
||||
def hw_main(seed_hosts, auto_hosts):
|
||||
if helpers.verbose >= 2:
|
||||
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())
|
||||
|
||||
for h in seed_hosts:
|
||||
@ -277,18 +213,22 @@ def hw_main(seed_hosts, auto_hosts):
|
||||
_enqueue(_check_netstat)
|
||||
check_host('localhost')
|
||||
check_host(socket.gethostname())
|
||||
check_workgroup('workgroup')
|
||||
check_workgroup('-')
|
||||
|
||||
while 1:
|
||||
now = time.time()
|
||||
# For each item in the queue
|
||||
for t, last_polled in list(queue.items()):
|
||||
(op, args) = t
|
||||
if not _stdin_still_ok(0):
|
||||
break
|
||||
|
||||
# Determine if we need to run.
|
||||
maxtime = POLL_TIME
|
||||
# netstat runs more often than other jobs
|
||||
if op == _check_netstat:
|
||||
maxtime = NETSTAT_POLL_TIME
|
||||
|
||||
# Check if this jobs needs to run.
|
||||
if now - last_polled > maxtime:
|
||||
queue[t] = time.time()
|
||||
op(*args)
|
||||
@ -298,5 +238,5 @@ def hw_main(seed_hosts, auto_hosts):
|
||||
break
|
||||
|
||||
# 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
|
||||
|
@ -1,15 +1,13 @@
|
||||
import re
|
||||
import os
|
||||
import socket
|
||||
import subprocess as ssubprocess
|
||||
from sshuttle.helpers import log, debug1, Fatal, family_to_string
|
||||
from sshuttle.helpers import log, debug1, Fatal, family_to_string, get_env
|
||||
|
||||
|
||||
def nonfatal(func, *args):
|
||||
try:
|
||||
func(*args)
|
||||
except Fatal as e:
|
||||
log('error: %s\n' % e)
|
||||
log('error: %s' % e)
|
||||
|
||||
|
||||
def ipt_chain_exists(family, table, name):
|
||||
@ -19,87 +17,35 @@ def ipt_chain_exists(family, table, name):
|
||||
cmd = 'iptables'
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||
argv = [cmd, '-t', table, '-nL']
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||
for line in p.stdout:
|
||||
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
|
||||
argv = [cmd, '-w', '-t', table, '-nL']
|
||||
try:
|
||||
output = ssubprocess.check_output(argv, env=get_env())
|
||||
for line in output.decode('ASCII').split('\n'):
|
||||
if line.startswith('Chain %s ' % name):
|
||||
return True
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
except ssubprocess.CalledProcessError as e:
|
||||
raise Fatal('%r returned %d' % (argv, e.returncode))
|
||||
|
||||
|
||||
def ipt(family, table, *args):
|
||||
if family == socket.AF_INET6:
|
||||
argv = ['ip6tables', '-t', table] + list(args)
|
||||
argv = ['ip6tables', '-w', '-t', table] + list(args)
|
||||
elif family == socket.AF_INET:
|
||||
argv = ['iptables', '-t', table] + list(args)
|
||||
argv = ['iptables', '-w', '-t', table] + list(args)
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
rv = ssubprocess.call(argv, env=env)
|
||||
debug1('%s' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv, env=get_env())
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def nft(family, table, action, *args):
|
||||
if family == socket.AF_INET:
|
||||
argv = ['nft', action, 'ip', table] + list(args)
|
||||
elif family == socket.AF_INET6:
|
||||
argv = ['nft', action, 'ip6', table] + list(args)
|
||||
if family in (socket.AF_INET, socket.AF_INET6):
|
||||
argv = ['nft', action, 'inet', table] + list(args)
|
||||
else:
|
||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
rv = ssubprocess.call(argv, env=env)
|
||||
debug1('%s' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv, env=get_env())
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def nft_get_handle(expression, chain):
|
||||
cmd = 'nft'
|
||||
argv = [cmd, 'list', expression, '-a']
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||
for line in p.stdout:
|
||||
if (b'jump %s' % chain.encode('utf-8')) in line:
|
||||
return re.sub('.*# ', '', line.decode('utf-8'))
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
_no_ttl_module = False
|
||||
|
||||
|
||||
def ipt_ttl(family, *args):
|
||||
global _no_ttl_module
|
||||
if not _no_ttl_module:
|
||||
# we avoid infinite loops by generating server-side connections
|
||||
# with ttl 42. This makes the client side not recapture those
|
||||
# connections, in case client == server.
|
||||
try:
|
||||
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
|
||||
ipt(family, *argsplus)
|
||||
except Fatal:
|
||||
ipt(family, *args)
|
||||
# we only get here if the non-ttl attempt succeeds
|
||||
log('sshuttle: warning: your iptables is missing '
|
||||
'the ttl module.\n')
|
||||
_no_ttl_module = True
|
||||
else:
|
||||
ipt(family, *args)
|
||||
|
@ -1,26 +1,35 @@
|
||||
import os
|
||||
import importlib
|
||||
import socket
|
||||
import struct
|
||||
import errno
|
||||
import ipaddress
|
||||
from sshuttle.helpers import Fatal, debug3
|
||||
|
||||
|
||||
def original_dst(sock):
|
||||
ip = "0.0.0.0"
|
||||
port = -1
|
||||
try:
|
||||
family = sock.family
|
||||
SO_ORIGINAL_DST = 80
|
||||
|
||||
if family == socket.AF_INET:
|
||||
SOCKADDR_MIN = 16
|
||||
sockaddr_in = sock.getsockopt(socket.SOL_IP,
|
||||
SO_ORIGINAL_DST, SOCKADDR_MIN)
|
||||
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
|
||||
# FIXME: decoding is IPv4 only.
|
||||
assert(socket.htons(proto) == socket.AF_INET)
|
||||
ip = '%d.%d.%d.%d' % (a, b, c, d)
|
||||
return (ip, port)
|
||||
port, raw_ip = struct.unpack_from('!2xH4s', sockaddr_in[:8])
|
||||
ip = str(ipaddress.IPv4Address(raw_ip))
|
||||
elif family == socket.AF_INET6:
|
||||
sockaddr_in = sock.getsockopt(41, SO_ORIGINAL_DST, 64)
|
||||
port, raw_ip = struct.unpack_from("!2xH4x16s", sockaddr_in)
|
||||
ip = str(ipaddress.IPv6Address(raw_ip))
|
||||
else:
|
||||
raise Fatal("fw: Unknown family type.")
|
||||
except socket.error as e:
|
||||
if e.args[0] == errno.ENOPROTOOPT:
|
||||
return sock.getsockname()
|
||||
raise
|
||||
return (ip, port)
|
||||
|
||||
|
||||
class Features(object):
|
||||
@ -38,19 +47,26 @@ class BaseMethod(object):
|
||||
@staticmethod
|
||||
def get_supported_features():
|
||||
result = Features()
|
||||
result.ipv4 = True
|
||||
result.ipv6 = False
|
||||
result.udp = False
|
||||
result.dns = True
|
||||
result.user = False
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def is_supported():
|
||||
"""Returns true if it appears that this method will work on this
|
||||
machine."""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_tcp_dstip(sock):
|
||||
return original_dst(sock)
|
||||
|
||||
@staticmethod
|
||||
def recv_udp(udp_listener, bufsize):
|
||||
debug3('Accept UDP using recvfrom.\n')
|
||||
debug3('Accept UDP using recvfrom.')
|
||||
data, srcip = udp_listener.recvfrom(bufsize)
|
||||
return (srcip, None, data)
|
||||
|
||||
@ -68,14 +84,14 @@ class BaseMethod(object):
|
||||
|
||||
def assert_features(self, features):
|
||||
avail = self.get_supported_features()
|
||||
for key in ["udp", "dns", "ipv6", "user"]:
|
||||
for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
|
||||
if getattr(features, key) and not getattr(avail, key):
|
||||
raise Fatal(
|
||||
"Feature %s not supported with method %s.\n" %
|
||||
"Feature %s not supported with method %s." %
|
||||
(key, self.name))
|
||||
|
||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||
user):
|
||||
user, tmark):
|
||||
raise NotImplementedError()
|
||||
|
||||
def restore_firewall(self, port, family, udp, user):
|
||||
@ -86,30 +102,21 @@ class BaseMethod(object):
|
||||
return False
|
||||
|
||||
|
||||
def _program_exists(name):
|
||||
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
||||
for p in paths:
|
||||
fn = '%s/%s' % (p, name)
|
||||
if os.path.exists(fn):
|
||||
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
||||
|
||||
|
||||
def get_method(method_name):
|
||||
module = importlib.import_module("sshuttle.methods.%s" % method_name)
|
||||
return module.Method(method_name)
|
||||
|
||||
|
||||
def get_auto_method():
|
||||
if _program_exists('iptables'):
|
||||
method_name = "nat"
|
||||
elif _program_exists('nft'):
|
||||
method_name = "nft"
|
||||
elif _program_exists('pfctl'):
|
||||
method_name = "pf"
|
||||
elif _program_exists('ipfw'):
|
||||
method_name = "ipfw"
|
||||
else:
|
||||
raise Fatal(
|
||||
"can't find either iptables, nft or pfctl; check your PATH")
|
||||
debug3("Selecting a method automatically...")
|
||||
# Try these methods, in order:
|
||||
methods_to_try = ["nat", "nft", "pf", "ipfw"]
|
||||
for m in methods_to_try:
|
||||
method = get_method(m)
|
||||
if method.is_supported():
|
||||
debug3("Method '%s' was automatically selected." % m)
|
||||
return method
|
||||
|
||||
return get_method(method_name)
|
||||
raise Fatal("Unable to automatically find a supported method. Check that "
|
||||
"the appropriate programs are in your PATH. We tried "
|
||||
"methods: %s" % str(methods_to_try))
|
||||
|
@ -1,36 +1,21 @@
|
||||
import os
|
||||
import subprocess as ssubprocess
|
||||
from sshuttle.methods import BaseMethod
|
||||
from sshuttle.helpers import log, debug1, debug3, \
|
||||
Fatal, family_to_string
|
||||
from sshuttle.helpers import log, debug1, debug2, debug3, \
|
||||
Fatal, family_to_string, get_env, which
|
||||
|
||||
recvmsg = None
|
||||
try:
|
||||
# try getting recvmsg from python
|
||||
import socket as pythonsocket
|
||||
getattr(pythonsocket.socket, "recvmsg")
|
||||
socket = pythonsocket
|
||||
recvmsg = "python"
|
||||
except AttributeError:
|
||||
# try getting recvmsg from socket_ext library
|
||||
try:
|
||||
import socket_ext
|
||||
getattr(socket_ext.socket, "recvmsg")
|
||||
socket = socket_ext
|
||||
recvmsg = "socket_ext"
|
||||
except ImportError:
|
||||
import socket
|
||||
import socket
|
||||
|
||||
IP_BINDANY = 24
|
||||
IP_RECVDSTADDR = 7
|
||||
SOL_IPV6 = 41
|
||||
IPV6_RECVDSTADDR = 74
|
||||
|
||||
if recvmsg == "python":
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP python using recvmsg.\n')
|
||||
data, ancdata, _, srcip = \
|
||||
listener.recvmsg(4096, socket.CMSG_SPACE(4))
|
||||
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP python using recvmsg.')
|
||||
data, ancdata, _, srcip = listener.recvmsg(4096,
|
||||
socket.CMSG_SPACE(4))
|
||||
dstip = None
|
||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
||||
@ -39,41 +24,20 @@ if recvmsg == "python":
|
||||
dstip = (ip, port)
|
||||
break
|
||||
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):
|
||||
argv = ['ipfw', 'list']
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||
argv = ['ipfw', 'list', '%d' % n]
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
||||
|
||||
found = False
|
||||
for line in p.stdout:
|
||||
if line.startswith(b'%05d ' % n):
|
||||
if not ('ipttl 42' in line or 'check-state' in line):
|
||||
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
||||
if 'check-state :sshuttle' not in line:
|
||||
log('non-sshuttle ipfw rule: %r' % line.strip())
|
||||
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||
found = True
|
||||
break
|
||||
rv = p.wait()
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
@ -85,11 +49,7 @@ _oldctls = {}
|
||||
|
||||
def _fill_oldctls(prefix):
|
||||
argv = ['sysctl', prefix]
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
||||
for line in p.stdout:
|
||||
line = line.decode()
|
||||
assert(line[-1] == '\n')
|
||||
@ -104,8 +64,9 @@ def _fill_oldctls(prefix):
|
||||
|
||||
def _sysctl_set(name, val):
|
||||
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
return ssubprocess.call(argv, stdout=open('/dev/null', 'w'))
|
||||
debug1('>> %s' % ' '.join(argv))
|
||||
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
|
||||
# No env: No output. (Or error that won't be parsed.)
|
||||
|
||||
|
||||
_changedctls = []
|
||||
@ -118,13 +79,13 @@ def sysctl_set(name, val, permanent=False):
|
||||
if not _oldctls:
|
||||
_fill_oldctls(PREFIX)
|
||||
if not (name in _oldctls):
|
||||
debug1('>> No such sysctl: %r\n' % name)
|
||||
debug1('>> No such sysctl: %r' % name)
|
||||
return False
|
||||
oldval = _oldctls[name]
|
||||
if val != oldval:
|
||||
rv = _sysctl_set(name, val)
|
||||
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.write('\n'
|
||||
'# Added by sshuttle\n'
|
||||
@ -134,25 +95,29 @@ def sysctl_set(name, val, permanent=False):
|
||||
_changedctls.append(name)
|
||||
return True
|
||||
|
||||
|
||||
def ipfw(*args):
|
||||
argv = ['ipfw', '-q'] + list(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv)
|
||||
debug1('>> %s' % ' '.join(argv))
|
||||
rv = ssubprocess.call(argv, env=get_env())
|
||||
# No env: No output. (Or error that won't be parsed.)
|
||||
if rv:
|
||||
raise Fatal('%r returned %d' % (argv, rv))
|
||||
|
||||
|
||||
def ipfw_noexit(*args):
|
||||
argv = ['ipfw', '-q'] + list(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
ssubprocess.call(argv)
|
||||
debug1('>> %s' % ' '.join(argv))
|
||||
ssubprocess.call(argv, env=get_env())
|
||||
# No env: No output. (Or error that won't be parsed.)
|
||||
|
||||
|
||||
class Method(BaseMethod):
|
||||
|
||||
def get_supported_features(self):
|
||||
result = super(Method, self).get_supported_features()
|
||||
result.ipv6 = False
|
||||
result.udp = False #NOTE: Almost there, kernel patch needed
|
||||
result.udp = False # NOTE: Almost there, kernel patch needed
|
||||
result.dns = True
|
||||
return result
|
||||
|
||||
@ -164,7 +129,7 @@ class Method(BaseMethod):
|
||||
if not dstip:
|
||||
debug1(
|
||||
"-- ignored UDP from %r: "
|
||||
"couldn't determine destination IP address\n" % (srcip,))
|
||||
"couldn't determine destination IP address" % (srcip,))
|
||||
return None
|
||||
return srcip, dstip, data
|
||||
|
||||
@ -172,34 +137,33 @@ class Method(BaseMethod):
|
||||
if not srcip:
|
||||
debug1(
|
||||
"-- ignored UDP to %r: "
|
||||
"couldn't determine source IP address\n" % (dstip,))
|
||||
"couldn't determine source IP address" % (dstip,))
|
||||
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.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
sender.bind(srcip)
|
||||
sender.sendto(data,dstip)
|
||||
sender.sendto(data, dstip)
|
||||
sender.close()
|
||||
|
||||
def setup_udp_listener(self, udp_listener):
|
||||
if udp_listener.v4 is not None:
|
||||
udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1)
|
||||
#if udp_listener.v6 is not None:
|
||||
# if udp_listener.v6 is not None:
|
||||
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
||||
|
||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||
user):
|
||||
user, tmark):
|
||||
# IPv6 not supported
|
||||
if family not in [socket.AF_INET]:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by ipfw method_name'
|
||||
% family_to_string(family))
|
||||
|
||||
#XXX: Any risk from this?
|
||||
# XXX: Any risk from this?
|
||||
ipfw_noexit('delete', '1')
|
||||
|
||||
while _changedctls:
|
||||
@ -210,8 +174,7 @@ class Method(BaseMethod):
|
||||
if subnets or dnsport:
|
||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||
|
||||
ipfw('add', '1', 'check-state', 'ip',
|
||||
'from', 'any', 'to', 'any')
|
||||
ipfw('add', '1', 'check-state', ':sshuttle')
|
||||
|
||||
ipfw('add', '1', 'skipto', '2',
|
||||
'tcp',
|
||||
@ -219,7 +182,7 @@ class Method(BaseMethod):
|
||||
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
|
||||
'tcp',
|
||||
'from', 'any', 'to', 'table(126)',
|
||||
'not', 'ipttl', '42', 'keep-state', 'setup')
|
||||
'setup', 'keep-state', ':sshuttle')
|
||||
|
||||
ipfw_noexit('table', '124', 'flush')
|
||||
dnscount = 0
|
||||
@ -230,15 +193,14 @@ class Method(BaseMethod):
|
||||
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
|
||||
'udp',
|
||||
'from', 'any', 'to', 'table(124)',
|
||||
'not', 'ipttl', '42')
|
||||
'keep-state', ':sshuttle')
|
||||
ipfw('add', '1', 'allow',
|
||||
'udp',
|
||||
'from', 'any', 'to', 'any',
|
||||
'ipttl', '42')
|
||||
'from', 'any', 'to', 'any')
|
||||
|
||||
if subnets:
|
||||
# create new subnet entries
|
||||
for _, swidth, sexclude, snet \
|
||||
for _, swidth, sexclude, snet, fport, lport \
|
||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||
if sexclude:
|
||||
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
||||
@ -248,10 +210,17 @@ class Method(BaseMethod):
|
||||
def restore_firewall(self, port, family, udp, user):
|
||||
if family not in [socket.AF_INET]:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by tproxy method'
|
||||
'Address family "%s" unsupported by ipfw method'
|
||||
% family_to_string(family))
|
||||
|
||||
ipfw_noexit('delete', '1')
|
||||
ipfw_noexit('table', '124', 'flush')
|
||||
ipfw_noexit('table', '125', 'flush')
|
||||
ipfw_noexit('table', '126', 'flush')
|
||||
|
||||
def is_supported(self):
|
||||
if which("ipfw"):
|
||||
return True
|
||||
debug2("ipfw method not supported because 'ipfw' command is "
|
||||
"missing.")
|
||||
return False
|
||||
|
@ -1,7 +1,7 @@
|
||||
import socket
|
||||
from sshuttle.firewall import subnet_weight
|
||||
from sshuttle.helpers import family_to_string
|
||||
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
|
||||
from sshuttle.helpers import family_to_string, which, debug2
|
||||
from sshuttle.linux import ipt, ipt_chain_exists, nonfatal
|
||||
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
|
||||
# "-A OUTPUT").
|
||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||
user):
|
||||
# only ipv4 supported with NAT
|
||||
if family != socket.AF_INET:
|
||||
user, tmark):
|
||||
if family != socket.AF_INET and family != socket.AF_INET6:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by nat method_name'
|
||||
% family_to_string(family))
|
||||
if udp:
|
||||
raise Exception("UDP not supported by nat method_name")
|
||||
|
||||
table = "nat"
|
||||
|
||||
def _ipt(*args):
|
||||
return ipt(family, table, *args)
|
||||
|
||||
def _ipt_ttl(*args):
|
||||
return ipt_ttl(family, table, *args)
|
||||
|
||||
def _ipm(*args):
|
||||
return ipt(family, "mangle", *args)
|
||||
|
||||
@ -50,6 +45,20 @@ class Method(BaseMethod):
|
||||
_ipt('-I', 'OUTPUT', '1', *args)
|
||||
_ipt('-I', 'PREROUTING', '1', *args)
|
||||
|
||||
# Redirect DNS traffic as requested. This includes routing traffic
|
||||
# to localhost DNS servers through sshuttle.
|
||||
for _, ip in [i for i in nslist if i[0] == family]:
|
||||
_ipt('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s' % ip,
|
||||
'-p', 'udp',
|
||||
'--dport', '53',
|
||||
'--to-ports', str(dnsport))
|
||||
|
||||
# Don't route any remaining local traffic through sshuttle.
|
||||
_ipt('-A', chain, '-j', 'RETURN',
|
||||
'-m', 'addrtype',
|
||||
'--dst-type', 'LOCAL')
|
||||
|
||||
# create new subnet entries.
|
||||
for _, swidth, sexclude, snet, fport, lport \
|
||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||
@ -62,20 +71,13 @@ class Method(BaseMethod):
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
*tcp_ports)
|
||||
else:
|
||||
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||
_ipt('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
*(tcp_ports + ('--to-ports', str(port))))
|
||||
|
||||
for _, ip in [i for i in nslist if i[0] == family]:
|
||||
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||
'--dest', '%s/32' % ip,
|
||||
'-p', 'udp',
|
||||
'--dport', '53',
|
||||
'--to-ports', str(dnsport))
|
||||
|
||||
def restore_firewall(self, port, family, udp, user):
|
||||
# only ipv4 supported with NAT
|
||||
if family != socket.AF_INET:
|
||||
if family != socket.AF_INET and family != socket.AF_INET6:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by nat method_name'
|
||||
% family_to_string(family))
|
||||
@ -87,9 +89,6 @@ class Method(BaseMethod):
|
||||
def _ipt(*args):
|
||||
return ipt(family, table, *args)
|
||||
|
||||
def _ipt_ttl(*args):
|
||||
return ipt_ttl(family, table, *args)
|
||||
|
||||
def _ipm(*args):
|
||||
return ipt(family, "mangle", *args)
|
||||
|
||||
@ -111,4 +110,12 @@ class Method(BaseMethod):
|
||||
def get_supported_features(self):
|
||||
result = super(Method, self).get_supported_features()
|
||||
result.user = True
|
||||
result.ipv6 = True
|
||||
return result
|
||||
|
||||
def is_supported(self):
|
||||
if which("iptables"):
|
||||
return True
|
||||
debug2("nat method not supported because 'iptables' command "
|
||||
"is missing.")
|
||||
return False
|
||||
|
@ -1,7 +1,8 @@
|
||||
import socket
|
||||
from sshuttle.firewall import subnet_weight
|
||||
from sshuttle.linux import nft, nft_get_handle, nonfatal
|
||||
from sshuttle.linux import nft, nonfatal
|
||||
from sshuttle.methods import BaseMethod
|
||||
from sshuttle.helpers import debug2, which
|
||||
|
||||
|
||||
class Method(BaseMethod):
|
||||
@ -12,23 +13,24 @@ class Method(BaseMethod):
|
||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||
# "-A OUTPUT").
|
||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||
user):
|
||||
user, tmark):
|
||||
if udp:
|
||||
raise Exception("UDP not supported by nft")
|
||||
|
||||
table = "nat"
|
||||
if family == socket.AF_INET:
|
||||
table = 'sshuttle-ipv4-%s' % port
|
||||
if family == socket.AF_INET6:
|
||||
table = 'sshuttle-ipv6-%s' % port
|
||||
|
||||
def _nft(action, *args):
|
||||
return nft(family, table, action, *args)
|
||||
|
||||
chain = 'sshuttle-%s' % port
|
||||
chain = table
|
||||
|
||||
# basic cleanup/setup of chains
|
||||
_nft('add table', '')
|
||||
_nft('add chain', 'prerouting',
|
||||
'{ type nat hook prerouting priority -100; policy accept; }')
|
||||
_nft('add chain', 'postrouting',
|
||||
'{ type nat hook postrouting priority 100; policy accept; }')
|
||||
_nft('add chain', 'output',
|
||||
'{ type nat hook output priority -100; policy accept; }')
|
||||
_nft('add chain', chain)
|
||||
@ -36,45 +38,77 @@ class Method(BaseMethod):
|
||||
_nft('add rule', 'output jump %s' % chain)
|
||||
_nft('add rule', 'prerouting jump %s' % chain)
|
||||
|
||||
# setup_firewall() gets called separately for ipv4 and ipv6. Make sure
|
||||
# we only handle the version that we expect to.
|
||||
if family == socket.AF_INET:
|
||||
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv4', 'return')
|
||||
else:
|
||||
_nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return')
|
||||
|
||||
# Strings to use below to simplify our code
|
||||
if family == socket.AF_INET:
|
||||
ip_version_l = 'ipv4'
|
||||
ip_version = 'ip'
|
||||
elif family == socket.AF_INET6:
|
||||
ip_version_l = 'ipv6'
|
||||
ip_version = 'ip6'
|
||||
|
||||
# Redirect DNS traffic as requested. This includes routing traffic
|
||||
# to localhost DNS servers through sshuttle.
|
||||
for _, ip in [i for i in nslist if i[0] == family]:
|
||||
_nft('add rule', chain, ip_version,
|
||||
'daddr %s' % ip, 'udp dport 53',
|
||||
('redirect to :' + str(dnsport)))
|
||||
|
||||
# Don't route any remaining local traffic through sshuttle
|
||||
_nft('add rule', chain, 'fib daddr type local return')
|
||||
|
||||
# create new subnet entries.
|
||||
for _, swidth, sexclude, snet, fport, lport \
|
||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||
tcp_ports = ('ip', 'protocol', 'tcp')
|
||||
if fport:
|
||||
tcp_ports = tcp_ports + ('dport { %d-%d }' % (fport, lport))
|
||||
|
||||
# match using nfproto as described at
|
||||
# https://superuser.com/questions/1560376/match-ipv6-protocol-using-nftables
|
||||
if fport and fport != lport:
|
||||
tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp',
|
||||
'dport', '{ %d-%d }' % (fport, lport))
|
||||
elif fport and fport == lport:
|
||||
tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp',
|
||||
'dport', '%d' % (fport))
|
||||
else:
|
||||
tcp_ports = ('meta', 'nfproto', ip_version_l,
|
||||
'meta', 'l4proto', 'tcp')
|
||||
|
||||
if sexclude:
|
||||
_nft('add rule', chain, *(tcp_ports + (
|
||||
'ip daddr %s/%s' % (snet, swidth), 'return')))
|
||||
ip_version, 'daddr %s/%s' % (snet, swidth), 'return')))
|
||||
else:
|
||||
_nft('add rule', chain, *(tcp_ports + (
|
||||
'ip daddr %s/%s' % (snet, swidth), 'ip ttl != 42',
|
||||
ip_version, 'daddr %s/%s' % (snet, swidth),
|
||||
('redirect to :' + str(port)))))
|
||||
|
||||
for _, ip in [i for i in nslist if i[0] == family]:
|
||||
if family == socket.AF_INET:
|
||||
_nft('add rule', chain, 'ip protocol udp ip daddr %s' % ip,
|
||||
'udp dport { 53 }', 'ip ttl != 42',
|
||||
('redirect to :' + str(dnsport)))
|
||||
elif family == socket.AF_INET6:
|
||||
_nft('add rule', chain, 'ip6 protocol udp ip6 daddr %s' % ip,
|
||||
'udp dport { 53 }', 'ip ttl != 42',
|
||||
('redirect to :' + str(dnsport)))
|
||||
|
||||
def restore_firewall(self, port, family, udp, user):
|
||||
if udp:
|
||||
raise Exception("UDP not supported by nft method_name")
|
||||
|
||||
table = "nat"
|
||||
if family == socket.AF_INET:
|
||||
table = 'sshuttle-ipv4-%s' % port
|
||||
if family == socket.AF_INET6:
|
||||
table = 'sshuttle-ipv6-%s' % port
|
||||
|
||||
def _nft(action, *args):
|
||||
return nft(family, table, action, *args)
|
||||
|
||||
chain = 'sshuttle-%s' % port
|
||||
|
||||
# basic cleanup/setup of chains
|
||||
handle = nft_get_handle('chain ip nat output', chain)
|
||||
nonfatal(_nft, 'delete rule', 'output', handle)
|
||||
handle = nft_get_handle('chain ip nat prerouting', chain)
|
||||
nonfatal(_nft, 'delete rule', 'prerouting', handle)
|
||||
nonfatal(_nft, 'delete chain', chain)
|
||||
nonfatal(_nft, 'delete table', '')
|
||||
|
||||
def get_supported_features(self):
|
||||
result = super(Method, self).get_supported_features()
|
||||
result.ipv6 = True
|
||||
return result
|
||||
|
||||
def is_supported(self):
|
||||
if which("nft"):
|
||||
return True
|
||||
debug2("nft method not supported because 'nft' command is missing.")
|
||||
return False
|
||||
|
@ -3,6 +3,7 @@ import sys
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import errno
|
||||
import struct
|
||||
import subprocess as ssubprocess
|
||||
import shlex
|
||||
@ -10,7 +11,8 @@ from fcntl import ioctl
|
||||
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
||||
sizeof, addressof, memmove
|
||||
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, \
|
||||
family_to_string, get_env, which
|
||||
from sshuttle.methods import BaseMethod
|
||||
|
||||
|
||||
@ -124,12 +126,14 @@ class Generic(object):
|
||||
memmove(addressof(pr) + self.RULE_ACTION_OFFSET,
|
||||
struct.pack('I', kind), 4) # rule.action = kind
|
||||
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
|
||||
'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET,
|
||||
struct.pack('I', self.PF_CHANGE_GET_TICKET),
|
||||
4) # action = PF_CHANGE_GET_TICKET
|
||||
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
|
||||
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
|
||||
'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
|
||||
memmove(addressof(pr) + self.ACTION_OFFSET,
|
||||
struct.pack('I', self.PF_CHANGE_ADD_TAIL),
|
||||
4) # action = PF_CHANGE_ADD_TAIL
|
||||
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
|
||||
|
||||
@staticmethod
|
||||
@ -151,7 +155,6 @@ class Generic(object):
|
||||
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
|
||||
|
||||
|
||||
|
||||
class FreeBsd(Generic):
|
||||
RULE_ACTION_OFFSET = 2968
|
||||
|
||||
@ -177,7 +180,8 @@ class FreeBsd(Generic):
|
||||
return freebsd
|
||||
|
||||
def enable(self):
|
||||
returncode = ssubprocess.call(['kldload', 'pf'])
|
||||
returncode = ssubprocess.call(['kldload', 'pf'], env=get_env())
|
||||
# No env: No output.
|
||||
super(FreeBsd, self).enable()
|
||||
if returncode == 0:
|
||||
_pf_context['loaded_by_sshuttle'] = True
|
||||
@ -186,7 +190,8 @@ class FreeBsd(Generic):
|
||||
super(FreeBsd, self).disable(anchor)
|
||||
if _pf_context['loaded_by_sshuttle'] and \
|
||||
_pf_context['started_by_sshuttle'] == 0:
|
||||
ssubprocess.call(['kldunload', 'pf'])
|
||||
ssubprocess.call(['kldunload', 'pf'], env=get_env())
|
||||
# No env: No output.
|
||||
|
||||
def add_anchors(self, anchor):
|
||||
status = pfctl('-s all')[0]
|
||||
@ -217,7 +222,7 @@ class FreeBsd(Generic):
|
||||
b'pass out route-to lo0 %s proto tcp '
|
||||
b'to %s keep state' % (inet_version, subnet)
|
||||
if not exclude else
|
||||
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
|
||||
b'pass out %s proto tcp to %s' % (inet_version, subnet)
|
||||
for exclude, subnet in includes
|
||||
]
|
||||
|
||||
@ -261,7 +266,7 @@ class OpenBsd(Generic):
|
||||
("proto_variant", c_uint8),
|
||||
("direction", c_uint8)]
|
||||
|
||||
self.pfioc_rule = c_char * 3400
|
||||
self.pfioc_rule = c_char * 3424
|
||||
self.pfioc_natlook = pfioc_natlook
|
||||
super(OpenBsd, self).__init__()
|
||||
|
||||
@ -287,7 +292,7 @@ class OpenBsd(Generic):
|
||||
b'pass out %s proto tcp to %s '
|
||||
b'route-to lo0 keep state' % (inet_version, subnet)
|
||||
if not exclude else
|
||||
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
|
||||
b'pass out %s proto tcp to %s' % (inet_version, subnet)
|
||||
for exclude, subnet in includes
|
||||
]
|
||||
|
||||
@ -381,18 +386,17 @@ else:
|
||||
|
||||
def pfctl(args, stdin=None):
|
||||
argv = ['pfctl'] + shlex.split(args)
|
||||
debug1('>> %s\n' % ' '.join(argv))
|
||||
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
debug1('>> %s' % ' '.join(argv))
|
||||
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
||||
stdout=ssubprocess.PIPE,
|
||||
stderr=ssubprocess.PIPE,
|
||||
env=env)
|
||||
env=get_env())
|
||||
o = p.communicate(stdin)
|
||||
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))
|
||||
|
||||
return o
|
||||
@ -420,7 +424,13 @@ class Method(BaseMethod):
|
||||
def get_tcp_dstip(self, sock):
|
||||
pfile = self.firewall.pfile
|
||||
|
||||
try:
|
||||
peer = sock.getpeername()
|
||||
except socket.error:
|
||||
_, e = sys.exc_info()[:2]
|
||||
if e.args[0] == errno.EINVAL:
|
||||
return sock.getsockname()
|
||||
|
||||
proxy = sock.getsockname()
|
||||
|
||||
argv = (sock.family, socket.IPPROTO_TCP,
|
||||
@ -438,7 +448,7 @@ class Method(BaseMethod):
|
||||
return sock.getsockname()
|
||||
|
||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||
user):
|
||||
user, tmark):
|
||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by pf method_name'
|
||||
@ -452,7 +462,7 @@ class Method(BaseMethod):
|
||||
# exclusion first; the table will ignore the second, opposite
|
||||
# definition
|
||||
for _, swidth, sexclude, snet, fport, lport \
|
||||
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||
in sorted(subnets, key=subnet_weight):
|
||||
includes.append((sexclude, b"%s/%d%s" % (
|
||||
snet.encode("ASCII"),
|
||||
swidth,
|
||||
@ -485,3 +495,9 @@ class Method(BaseMethod):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_supported(self):
|
||||
if which("pfctl"):
|
||||
return True
|
||||
debug2("pf method not supported because 'pfctl' command is missing.")
|
||||
return False
|
||||
|
@ -1,26 +1,12 @@
|
||||
import struct
|
||||
from sshuttle.firewall import subnet_weight
|
||||
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.helpers import debug1, debug3, Fatal
|
||||
from sshuttle.helpers import debug1, debug2, debug3, Fatal, which
|
||||
|
||||
recvmsg = None
|
||||
try:
|
||||
# try getting recvmsg from python
|
||||
import socket as pythonsocket
|
||||
getattr(pythonsocket.socket, "recvmsg")
|
||||
socket = pythonsocket
|
||||
recvmsg = "python"
|
||||
except AttributeError:
|
||||
# try getting recvmsg from socket_ext library
|
||||
try:
|
||||
import socket_ext
|
||||
getattr(socket_ext.socket, "recvmsg")
|
||||
socket = socket_ext
|
||||
recvmsg = "socket_ext"
|
||||
except ImportError:
|
||||
import socket
|
||||
import socket
|
||||
import os
|
||||
|
||||
|
||||
IP_TRANSPARENT = 19
|
||||
@ -30,9 +16,9 @@ SOL_IPV6 = 41
|
||||
IPV6_ORIGDSTADDR = 74
|
||||
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
||||
|
||||
if recvmsg == "python":
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP python using recvmsg.\n')
|
||||
|
||||
def recv_udp(listener, bufsize):
|
||||
debug3('Accept UDP python using recvmsg.')
|
||||
data, ancdata, _, srcip = listener.recvmsg(
|
||||
4096, socket.CMSG_SPACE(24))
|
||||
dstip = None
|
||||
@ -61,44 +47,6 @@ if recvmsg == "python":
|
||||
dstip = (ip, port)
|
||||
break
|
||||
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):
|
||||
@ -106,10 +54,6 @@ class Method(BaseMethod):
|
||||
def get_supported_features(self):
|
||||
result = super(Method, self).get_supported_features()
|
||||
result.ipv6 = True
|
||||
if recvmsg is None:
|
||||
result.udp = False
|
||||
result.dns = False
|
||||
else:
|
||||
result.udp = True
|
||||
result.dns = True
|
||||
return result
|
||||
@ -126,6 +70,15 @@ class Method(BaseMethod):
|
||||
return None
|
||||
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):
|
||||
if not srcip:
|
||||
debug1(
|
||||
@ -134,16 +87,26 @@ class Method(BaseMethod):
|
||||
return
|
||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||
except PermissionError as e:
|
||||
self.setsockopt_error(e)
|
||||
sender.bind(srcip)
|
||||
sender.sendto(data, dstip)
|
||||
sender.close()
|
||||
|
||||
def setup_tcp_listener(self, tcp_listener):
|
||||
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):
|
||||
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:
|
||||
udp_listener.v4.setsockopt(
|
||||
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
||||
@ -151,7 +114,7 @@ class Method(BaseMethod):
|
||||
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
||||
|
||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||
user):
|
||||
user, tmark):
|
||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||
raise Exception(
|
||||
'Address family "%s" unsupported by tproxy method'
|
||||
@ -162,14 +125,10 @@ class Method(BaseMethod):
|
||||
def _ipt(*args):
|
||||
return ipt(family, table, *args)
|
||||
|
||||
def _ipt_ttl(*args):
|
||||
return ipt_ttl(family, table, *args)
|
||||
|
||||
def _ipt_proto_ports(proto, fport, lport):
|
||||
return proto + ('--dport', '%d:%d' % (fport, lport)) \
|
||||
if fport else proto
|
||||
|
||||
|
||||
mark_chain = 'sshuttle-m-%s' % port
|
||||
tproxy_chain = 'sshuttle-t-%s' % port
|
||||
divert_chain = 'sshuttle-d-%s' % port
|
||||
@ -185,7 +144,23 @@ class Method(BaseMethod):
|
||||
_ipt('-F', tproxy_chain)
|
||||
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
|
||||
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
|
||||
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
|
||||
|
||||
# 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', 'ACCEPT')
|
||||
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||
'-m', 'tcp', '-p', 'tcp')
|
||||
@ -195,11 +170,11 @@ class Method(BaseMethod):
|
||||
'-m', 'udp', '-p', 'udp')
|
||||
|
||||
for _, ip in [i for i in nslist if i[0] == family]:
|
||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
|
||||
'--dest', '%s/32' % ip,
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1',
|
||||
'--tproxy-mark', tmark,
|
||||
'--dest', '%s/32' % ip,
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
||||
'--on-port', str(dnsport))
|
||||
@ -219,12 +194,12 @@ class Method(BaseMethod):
|
||||
'-m', 'tcp',
|
||||
*tcp_ports)
|
||||
else:
|
||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'tcp',
|
||||
*tcp_ports)
|
||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1',
|
||||
'--tproxy-mark', tmark,
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'tcp',
|
||||
*(tcp_ports + ('--on-port', str(port))))
|
||||
@ -243,11 +218,12 @@ class Method(BaseMethod):
|
||||
'-m', 'udp',
|
||||
*udp_ports)
|
||||
else:
|
||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark,
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'udp', '-p', 'udp')
|
||||
'-m', 'udp',
|
||||
*udp_ports)
|
||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1',
|
||||
'--tproxy-mark', tmark,
|
||||
'--dest', '%s/%s' % (snet, swidth),
|
||||
'-m', 'udp',
|
||||
*(udp_ports + ('--on-port', str(port))))
|
||||
@ -263,9 +239,6 @@ class Method(BaseMethod):
|
||||
def _ipt(*args):
|
||||
return ipt(family, table, *args)
|
||||
|
||||
def _ipt_ttl(*args):
|
||||
return ipt_ttl(family, table, *args)
|
||||
|
||||
mark_chain = 'sshuttle-m-%s' % port
|
||||
tproxy_chain = 'sshuttle-t-%s' % port
|
||||
divert_chain = 'sshuttle-d-%s' % port
|
||||
@ -284,3 +257,10 @@ class Method(BaseMethod):
|
||||
if ipt_chain_exists(family, table, divert_chain):
|
||||
_ipt('-F', divert_chain)
|
||||
_ipt('-X', divert_chain)
|
||||
|
||||
def is_supported(self):
|
||||
if which("iptables") and which("ip6tables"):
|
||||
return True
|
||||
debug2("tproxy method not supported because 'iptables' "
|
||||
"or 'ip6tables' commands are missing.\n")
|
||||
return False
|
||||
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import socket
|
||||
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
|
||||
|
||||
from sshuttle import __version__
|
||||
|
||||
|
||||
@ -27,29 +28,73 @@ def parse_subnetport_file(s):
|
||||
# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
|
||||
# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
|
||||
# example.com:123 or just example.com
|
||||
#
|
||||
# In addition, the port number can be specified as a range:
|
||||
# 1.2.3.4:8000-8080.
|
||||
#
|
||||
# Can return multiple matches if the domain name used in the request
|
||||
# has multiple IP addresses.
|
||||
def parse_subnetport(s):
|
||||
|
||||
if s.count(':') > 1:
|
||||
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
|
||||
else:
|
||||
rx = r'([\w\.]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
|
||||
rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
|
||||
|
||||
m = re.match(rx, s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid address/mask:port format' % s)
|
||||
|
||||
addr, width, fport, lport = m.groups()
|
||||
# Ports range from fport to lport. If only one port is specified,
|
||||
# fport is defined and lport is None.
|
||||
#
|
||||
# cidr is the mask defined with the slash notation
|
||||
host, cidr, fport, lport = m.groups()
|
||||
try:
|
||||
addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
|
||||
addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM)
|
||||
except socket.gaierror:
|
||||
raise Fatal('Unable to resolve address: %s' % addr)
|
||||
raise Fatal('Unable to resolve address: %s' % host)
|
||||
|
||||
family, _, _, _, addr = min(addrinfo)
|
||||
max_width = 32 if family == socket.AF_INET else 128
|
||||
width = int(width or max_width)
|
||||
if not 0 <= width <= max_width:
|
||||
raise Fatal('width %d is not between 0 and %d' % (width, max_width))
|
||||
# If the address is a domain with multiple IPs and a mask is also
|
||||
# provided, proceed cautiously:
|
||||
if cidr is not None:
|
||||
addr_v6 = [a for a in addrinfo if a[0] == socket.AF_INET6]
|
||||
addr_v4 = [a for a in addrinfo if a[0] == socket.AF_INET]
|
||||
|
||||
return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
|
||||
# Refuse to proceed if IPv4 and IPv6 addresses are present:
|
||||
if len(addr_v6) > 0 and len(addr_v4) > 0:
|
||||
raise Fatal("%s has IPv4 and IPv6 addresses, so the mask "
|
||||
"of /%s is not supported. Specify the IP "
|
||||
"addresses directly if you wish to specify "
|
||||
"a mask." % (host, cidr))
|
||||
|
||||
# Warn if a domain has multiple IPs of the same type (IPv4 vs
|
||||
# IPv6) and the mask is applied to all of the IPs.
|
||||
if len(addr_v4) > 1 or len(addr_v6) > 1:
|
||||
print("WARNING: %s has multiple IP addresses. The "
|
||||
"mask of /%s is applied to all of the addresses."
|
||||
% (host, cidr))
|
||||
|
||||
rv = []
|
||||
for a in addrinfo:
|
||||
family, _, _, _, addr = a
|
||||
|
||||
# Largest possible slash value we can use with this IP:
|
||||
max_cidr = 32 if family == socket.AF_INET else 128
|
||||
|
||||
if cidr is None: # if no mask, use largest mask
|
||||
cidr_to_use = max_cidr
|
||||
else: # verify user-provided mask is appropriate
|
||||
cidr_to_use = int(cidr)
|
||||
if not 0 <= cidr_to_use <= max_cidr:
|
||||
raise Fatal('Slash in CIDR notation (/%d) is '
|
||||
'not between 0 and %d'
|
||||
% (cidr_to_use, max_cidr))
|
||||
|
||||
rv.append((family, addr[0], cidr_to_use,
|
||||
int(fport or 0), int(lport or fport or 0)))
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
||||
@ -62,26 +107,32 @@ def parse_ipport(s):
|
||||
elif ']' in s:
|
||||
rx = r'(?:\[([^]]+)])(?::(\d+))?$'
|
||||
else:
|
||||
rx = r'([\w\.]+)(?::(\d+))?$'
|
||||
rx = r'([\w\.\-]+)(?::(\d+))?$'
|
||||
|
||||
m = re.match(rx, s)
|
||||
if not m:
|
||||
raise Fatal('%r is not a valid IP:port format' % s)
|
||||
|
||||
ip, port = m.groups()
|
||||
ip = ip or '0.0.0.0'
|
||||
host, port = m.groups()
|
||||
host = host or '0.0.0.0'
|
||||
port = int(port or 0)
|
||||
|
||||
try:
|
||||
addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
|
||||
addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
|
||||
except socket.gaierror:
|
||||
raise Fatal('%r is not a valid IP:port format' % s)
|
||||
raise Fatal('Unable to resolve address: %s' % host)
|
||||
|
||||
if len(addrinfo) > 1:
|
||||
print("WARNING: Host %s has more than one IP, only using one of them."
|
||||
% host)
|
||||
|
||||
family, _, _, _, addr = min(addrinfo)
|
||||
# Note: addr contains (ip, port)
|
||||
return (family,) + addr[:2]
|
||||
|
||||
|
||||
def parse_list(lst):
|
||||
"""Parse a comma separated string into a list."""
|
||||
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
||||
|
||||
|
||||
@ -96,9 +147,33 @@ class Concat(Action):
|
||||
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",
|
||||
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="@"
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -146,6 +221,7 @@ parser.add_argument(
|
||||
type=parse_list,
|
||||
help="""
|
||||
capture and forward DNS requests made to the following servers
|
||||
(comma separated)
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -176,9 +252,9 @@ parser.add_argument(
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--remote",
|
||||
metavar="[USERNAME@]ADDR[:PORT]",
|
||||
metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
|
||||
help="""
|
||||
ssh hostname (and optional username) of remote %(prog)s server
|
||||
ssh hostname (and optional username and password) of remote %(prog)s server
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -206,7 +282,7 @@ parser.add_argument(
|
||||
action="count",
|
||||
default=0,
|
||||
help="""
|
||||
increase debug message verbosity
|
||||
increase debug message verbosity (can be used more than once)
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -242,6 +318,16 @@ parser.add_argument(
|
||||
sacrifice latency to improve bandwidth benchmarks
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--latency-buffer-size",
|
||||
metavar="SIZE",
|
||||
type=int,
|
||||
default=32768,
|
||||
dest="latency_buffer_size",
|
||||
help="""
|
||||
size of latency control buffer
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wrap",
|
||||
metavar="NUM",
|
||||
@ -310,3 +396,51 @@ parser.add_argument(
|
||||
(internal use only)
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers",
|
||||
action="store_true",
|
||||
help="""
|
||||
Add sshuttle to the sudoers for this user
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers-no-modify",
|
||||
action="store_true",
|
||||
help="""
|
||||
Prints the sudoers config to STDOUT and DOES NOT modify anything.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers-user",
|
||||
default="",
|
||||
help="""
|
||||
Set the user name or group with %%group_name for passwordless operation.
|
||||
Default is the current user.set ALL for all users. Only works with
|
||||
--sudoers or --sudoers-no-modify option.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sudoers-filename",
|
||||
default="sshuttle_auto",
|
||||
help="""
|
||||
Set the file name for the sudoers.d file to be added. Default is
|
||||
"sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-sudo-pythonpath",
|
||||
action="store_false",
|
||||
dest="sudo_pythonpath",
|
||||
help="""
|
||||
do not set PYTHONPATH when invoking sudo
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t", "--tmark",
|
||||
metavar="[MARK]",
|
||||
default="0x01",
|
||||
help="""
|
||||
tproxy optional traffic mark with provided MARK value in
|
||||
hexadecimal (default '0x01')
|
||||
"""
|
||||
)
|
||||
|
@ -1,8 +1,21 @@
|
||||
"""When sshuttle is run via a systemd service file, we can communicate
|
||||
to systemd about the status of the sshuttle process. In particular, we
|
||||
can send READY status to tell systemd that sshuttle has completed
|
||||
startup and send STOPPING to indicate that sshuttle is beginning
|
||||
shutdown.
|
||||
|
||||
For details, see:
|
||||
https://www.freedesktop.org/software/systemd/man/sd_notify.html
|
||||
"""
|
||||
|
||||
import socket
|
||||
import os
|
||||
|
||||
from sshuttle.helpers import debug1
|
||||
|
||||
|
||||
def _notify(message):
|
||||
"""Send a notification message to systemd."""
|
||||
addr = os.environ.get("NOTIFY_SOCKET", None)
|
||||
|
||||
if not addr or len(addr) == 1 or addr[0] not in ('/', '@'):
|
||||
@ -13,7 +26,7 @@ def _notify(message):
|
||||
try:
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
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
|
||||
|
||||
if not message:
|
||||
@ -24,17 +37,27 @@ def _notify(message):
|
||||
try:
|
||||
return (sock.sendto(message, addr) > 0)
|
||||
except (OSError, IOError) as e:
|
||||
debug1("Error notifying systemd: %s\n" % e)
|
||||
debug1("Error notifying systemd: %s" % e)
|
||||
return False
|
||||
|
||||
|
||||
def send(*messages):
|
||||
"""Send multiple messages to systemd."""
|
||||
return _notify(b'\n'.join(messages))
|
||||
|
||||
|
||||
def ready():
|
||||
"""Constructs a message that is appropriate to send upon completion of
|
||||
sshuttle startup."""
|
||||
return b"READY=1"
|
||||
|
||||
|
||||
def stop():
|
||||
"""Constructs a message that is appropriate to send when sshuttle is
|
||||
beginning to shutdown."""
|
||||
return b"STOPPING=1"
|
||||
|
||||
|
||||
def status(message):
|
||||
"""Constructs a status message to be sent to systemd."""
|
||||
return b"STATUS=%s" % message.encode('utf8')
|
||||
|
@ -5,7 +5,7 @@ import traceback
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
|
||||
|
||||
import sshuttle.ssnet as ssnet
|
||||
import sshuttle.helpers as helpers
|
||||
@ -13,19 +13,14 @@ import sshuttle.hostwatch as hostwatch
|
||||
import subprocess as ssubprocess
|
||||
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
||||
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
|
||||
resolvconf_random_nameserver
|
||||
|
||||
try:
|
||||
from shutil import which
|
||||
except ImportError:
|
||||
from distutils.spawn import find_executable as which
|
||||
resolvconf_random_nameserver, which, get_env
|
||||
|
||||
|
||||
def _ipmatch(ipstr):
|
||||
# FIXME: IPv4 only
|
||||
if ipstr == 'default':
|
||||
ipstr = '0.0.0.0/0'
|
||||
m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||
if m:
|
||||
g = m.groups()
|
||||
ips = g[0]
|
||||
@ -86,11 +81,7 @@ def _route_iproute(line):
|
||||
|
||||
def _list_routes(argv, extract_route):
|
||||
# FIXME: IPv4 only
|
||||
env = {
|
||||
'PATH': os.environ['PATH'],
|
||||
'LC_ALL': "C",
|
||||
}
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
||||
routes = []
|
||||
for line in p.stdout:
|
||||
if not line.strip():
|
||||
@ -104,8 +95,7 @@ def _list_routes(argv, extract_route):
|
||||
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
|
||||
rv = p.wait()
|
||||
if rv != 0:
|
||||
log('WARNING: %r returned %d\n' % (argv, rv))
|
||||
log('WARNING: That prevents --auto-nets from working.\n')
|
||||
log('WARNING: %r returned %d' % (argv, rv))
|
||||
|
||||
return routes
|
||||
|
||||
@ -116,7 +106,8 @@ def list_routes():
|
||||
elif which('netstat'):
|
||||
routes = _list_routes(['netstat', '-rn'], _route_netstat)
|
||||
else:
|
||||
log('WARNING: Neither ip nor netstat were found on the server.\n')
|
||||
log('WARNING: Neither "ip" nor "netstat" were found on the server. '
|
||||
'--auto-nets feature will not work.')
|
||||
routes = []
|
||||
|
||||
for (family, ip, width) in routes:
|
||||
@ -143,7 +134,7 @@ def start_hostwatch(seed_hosts, auto_hosts):
|
||||
s1.close()
|
||||
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
|
||||
except Exception:
|
||||
log('%s\n' % _exc_dump())
|
||||
log('%s' % _exc_dump())
|
||||
rv = 98
|
||||
finally:
|
||||
os._exit(rv)
|
||||
@ -191,7 +182,7 @@ class DnsProxy(Handler):
|
||||
self.tries += 1
|
||||
|
||||
if self.to_nameserver is None:
|
||||
_, peer = resolvconf_random_nameserver()
|
||||
_, peer = resolvconf_random_nameserver(False)
|
||||
port = 53
|
||||
else:
|
||||
peer = self.to_ns_peer
|
||||
@ -199,12 +190,11 @@ class DnsProxy(Handler):
|
||||
|
||||
family, sockaddr = self._addrinfo(peer, port)
|
||||
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
sock.connect(sockaddr)
|
||||
|
||||
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:
|
||||
sock.send(self.request)
|
||||
self.socks.append(sock)
|
||||
@ -214,11 +204,11 @@ class DnsProxy(Handler):
|
||||
# might have been spurious; try again.
|
||||
# Note: these errors sometimes are reported by recv(),
|
||||
# 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()
|
||||
return
|
||||
else:
|
||||
log('DNS send to %r: %s\n' % (peer, e))
|
||||
log('DNS send to %r: %s' % (peer, e))
|
||||
return
|
||||
|
||||
def callback(self, sock):
|
||||
@ -235,13 +225,13 @@ class DnsProxy(Handler):
|
||||
# might have been spurious; try again.
|
||||
# Note: these errors sometimes are reported by recv(),
|
||||
# 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()
|
||||
return
|
||||
else:
|
||||
log('DNS recv from %r: %s\n' % (peer, e))
|
||||
log('DNS recv from %r: %s' % (peer, e))
|
||||
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.ok = False
|
||||
|
||||
@ -255,16 +245,14 @@ class UdpProxy(Handler):
|
||||
self.mux = mux
|
||||
self.chan = chan
|
||||
self.sock = sock
|
||||
if family == socket.AF_INET:
|
||||
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
|
||||
def send(self, dstip, data):
|
||||
debug2('UDP: sending to %r port %d\n' % dstip)
|
||||
debug2('UDP: sending to %r port %d' % dstip)
|
||||
try:
|
||||
self.sock.sendto(data, dstip)
|
||||
except socket.error:
|
||||
_, e = sys.exc_info()[:2]
|
||||
log('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
|
||||
|
||||
def callback(self, sock):
|
||||
@ -272,38 +260,40 @@ class UdpProxy(Handler):
|
||||
data, peer = sock.recvfrom(4096)
|
||||
except socket.error:
|
||||
_, e = sys.exc_info()[:2]
|
||||
log('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
|
||||
debug2('UDP response: %d bytes\n' % len(data))
|
||||
debug2('UDP response: %d bytes' % len(data))
|
||||
hdr = b("%s,%r," % (peer[0], peer[1]))
|
||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||
|
||||
|
||||
def main(latency_control, auto_hosts, to_nameserver):
|
||||
debug1('Starting server with Python version %s\n'
|
||||
% platform.python_version())
|
||||
|
||||
if helpers.verbose >= 1:
|
||||
def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
|
||||
auto_nets):
|
||||
try:
|
||||
helpers.logprefix = ' s: '
|
||||
else:
|
||||
helpers.logprefix = 'server: '
|
||||
debug1('latency control setting = %r\n' % latency_control)
|
||||
|
||||
routes = list(list_routes())
|
||||
debug1('available routes:\n')
|
||||
for r in routes:
|
||||
debug1(' %d/%s/%d\n' % r)
|
||||
debug1('latency control setting = %r' % latency_control)
|
||||
if latency_buffer_size:
|
||||
import sshuttle.ssnet as ssnet
|
||||
ssnet.LATENCY_BUFFER_SIZE = latency_buffer_size
|
||||
|
||||
# synchronization header
|
||||
sys.stdout.write('\0\0SSHUTTLE0001')
|
||||
sys.stdout.flush()
|
||||
|
||||
handlers = []
|
||||
mux = Mux(socket.fromfd(sys.stdin.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM),
|
||||
socket.fromfd(sys.stdout.fileno(),
|
||||
socket.AF_INET, socket.SOCK_STREAM))
|
||||
mux = Mux(sys.stdin, sys.stdout)
|
||||
handlers.append(mux)
|
||||
|
||||
debug1('auto-nets:' + str(auto_nets))
|
||||
if auto_nets:
|
||||
routes = list(list_routes())
|
||||
debug1('available routes:')
|
||||
for r in routes:
|
||||
debug1(' %d/%s/%d' % r)
|
||||
else:
|
||||
routes = []
|
||||
|
||||
routepkt = ''
|
||||
for r in routes:
|
||||
routepkt += '%d,%s,%d\n' % r
|
||||
@ -352,7 +342,7 @@ def main(latency_control, auto_hosts, to_nameserver):
|
||||
dnshandlers = {}
|
||||
|
||||
def dns_req(channel, data):
|
||||
debug2('Incoming DNS request channel=%d.\n' % channel)
|
||||
debug2('Incoming DNS request channel=%d.' % channel)
|
||||
h = DnsProxy(mux, channel, data, to_nameserver)
|
||||
handlers.append(h)
|
||||
dnshandlers[channel] = h
|
||||
@ -361,25 +351,28 @@ def main(latency_control, auto_hosts, to_nameserver):
|
||||
udphandlers = {}
|
||||
|
||||
def udp_req(channel, cmd, data):
|
||||
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
|
||||
debug2('Incoming UDP request channel=%d, cmd=%d' %
|
||||
(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.\n' % (dstip, 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\n')
|
||||
debug2('is incoming UDP close')
|
||||
h = udphandlers[channel]
|
||||
h.ok = False
|
||||
del mux.channels[channel]
|
||||
|
||||
def udp_open(channel, data):
|
||||
debug2('Incoming UDP open.\n')
|
||||
debug2('Incoming UDP open.')
|
||||
family = int(data)
|
||||
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
|
||||
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd,
|
||||
data)
|
||||
if channel in udphandlers:
|
||||
raise Fatal('UDP connection channel %d already open' % channel)
|
||||
raise Fatal('UDP connection channel %d already open' %
|
||||
channel)
|
||||
else:
|
||||
h = UdpProxy(mux, channel, family)
|
||||
handlers.append(h)
|
||||
@ -392,7 +385,7 @@ def main(latency_control, auto_hosts, to_nameserver):
|
||||
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
|
||||
if rpid:
|
||||
raise Fatal(
|
||||
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
|
||||
'hostwatch exited unexpectedly: code 0x%04x' % rv)
|
||||
|
||||
ssnet.runonce(handlers, mux)
|
||||
if latency_control:
|
||||
@ -403,7 +396,7 @@ def main(latency_control, auto_hosts, to_nameserver):
|
||||
remove = []
|
||||
for channel, h in dnshandlers.items():
|
||||
if h.timeout < now or not h.ok:
|
||||
debug3('expiring dnsreqs channel=%d\n' % channel)
|
||||
debug3('expiring dnsreqs channel=%d' % channel)
|
||||
remove.append(channel)
|
||||
h.ok = False
|
||||
for channel in remove:
|
||||
@ -412,8 +405,12 @@ def main(latency_control, auto_hosts, to_nameserver):
|
||||
remove = []
|
||||
for channel, h in udphandlers.items():
|
||||
if not h.ok:
|
||||
debug3('expiring UDP channel=%d\n' % channel)
|
||||
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)
|
||||
|
218
sshuttle/ssh.py
218
sshuttle/ssh.py
@ -3,89 +3,96 @@ import os
|
||||
import re
|
||||
import socket
|
||||
import zlib
|
||||
import imp
|
||||
import importlib
|
||||
import importlib.util
|
||||
import subprocess as ssubprocess
|
||||
import shlex
|
||||
from shlex import quote
|
||||
import ipaddress
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import sshuttle.helpers as helpers
|
||||
from sshuttle.helpers import debug2
|
||||
|
||||
try:
|
||||
# Python >= 3.5
|
||||
from shlex import quote
|
||||
except ImportError:
|
||||
# Python 2.x
|
||||
from pipes import quote
|
||||
from sshuttle.helpers import debug2, which, get_path, Fatal
|
||||
|
||||
|
||||
def readfile(name):
|
||||
tokens = name.split(".")
|
||||
f = None
|
||||
|
||||
token = tokens[0]
|
||||
token_name = [token]
|
||||
token_str = ".".join(token_name)
|
||||
|
||||
try:
|
||||
f, pathname, description = imp.find_module(token_str)
|
||||
|
||||
for token in tokens[1:]:
|
||||
module = imp.load_module(token_str, f, pathname, description)
|
||||
if f is not None:
|
||||
f.close()
|
||||
|
||||
token_name.append(token)
|
||||
token_str = ".".join(token_name)
|
||||
|
||||
f, pathname, description = imp.find_module(
|
||||
token, module.__path__)
|
||||
|
||||
if f is not None:
|
||||
contents = f.read()
|
||||
else:
|
||||
contents = ""
|
||||
|
||||
finally:
|
||||
if f is not None:
|
||||
f.close()
|
||||
|
||||
return contents.encode("UTF8")
|
||||
def get_module_source(name):
|
||||
spec = importlib.util.find_spec(name)
|
||||
with open(spec.origin, "rt") as f:
|
||||
return f.read().encode("utf-8")
|
||||
|
||||
|
||||
def empackage(z, name, data=None):
|
||||
if not data:
|
||||
data = readfile(name)
|
||||
data = get_module_source(name)
|
||||
content = z.compress(data)
|
||||
content += z.flush(zlib.Z_SYNC_FLUSH)
|
||||
|
||||
return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content)
|
||||
|
||||
|
||||
def parse_hostport(rhostport):
|
||||
"""
|
||||
parses the given rhostport variable, looking like this:
|
||||
|
||||
[username[:password]@]host[:port]
|
||||
|
||||
if only host is given, can be a hostname, IPv4/v6 address or a ssh alias
|
||||
from ~/.ssh/config
|
||||
|
||||
and returns a tuple (username, password, port, host)
|
||||
"""
|
||||
# leave use of default port to ssh command to prevent overwriting
|
||||
# ports configured in ~/.ssh/config when no port is given
|
||||
if rhostport is None or len(rhostport) == 0:
|
||||
return None, None, None, None
|
||||
port = None
|
||||
username = None
|
||||
password = None
|
||||
host = rhostport
|
||||
|
||||
if "@" in host:
|
||||
# split username (and possible password) from the host[:port]
|
||||
username, host = host.rsplit("@", 1)
|
||||
# Fix #410 bad username error detect
|
||||
if ":" in username:
|
||||
# this will even allow for the username to be empty
|
||||
username, password = username.split(":")
|
||||
|
||||
if ":" in host:
|
||||
# IPv6 address and/or got a port specified
|
||||
|
||||
# If it is an IPv6 address with port specification,
|
||||
# then it will look like: [::1]:22
|
||||
|
||||
try:
|
||||
# try to parse host as an IP address,
|
||||
# if that works it is an IPv6 address
|
||||
host = str(ipaddress.ip_address(host))
|
||||
except ValueError:
|
||||
# if that fails parse as URL to get the port
|
||||
parsed = urlparse('//{}'.format(host))
|
||||
try:
|
||||
host = str(ipaddress.ip_address(parsed.hostname))
|
||||
except ValueError:
|
||||
# else if both fails, we have a hostname with port
|
||||
host = parsed.hostname
|
||||
port = parsed.port
|
||||
|
||||
if password is None or len(password) == 0:
|
||||
password = None
|
||||
|
||||
return username, password, port, host
|
||||
|
||||
|
||||
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
portl = []
|
||||
|
||||
if re.sub(r'.*@', '', rhostport or '').count(':') > 1:
|
||||
if rhostport.count(']') or rhostport.count('['):
|
||||
result = rhostport.split(']')
|
||||
rhost = result[0].strip('[')
|
||||
if len(result) > 1:
|
||||
result[1] = result[1].strip(':')
|
||||
if result[1] != '':
|
||||
portl = ['-p', str(int(result[1]))]
|
||||
# can't disambiguate IPv6 colons and a port number. pass the hostname
|
||||
# through.
|
||||
username, password, port, host = parse_hostport(rhostport)
|
||||
if username:
|
||||
rhost = "{}@{}".format(username, host)
|
||||
else:
|
||||
rhost = rhostport
|
||||
else: # IPv4
|
||||
l = (rhostport or '').rsplit(':', 1)
|
||||
rhost = l[0]
|
||||
if len(l) > 1:
|
||||
portl = ['-p', str(int(l[1]))]
|
||||
|
||||
if rhost == '-':
|
||||
rhost = None
|
||||
rhost = host
|
||||
|
||||
z = zlib.compressobj(1)
|
||||
content = readfile('sshuttle.assembler')
|
||||
content = get_module_source('sshuttle.assembler')
|
||||
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
|
||||
optdata = optdata.encode("UTF8")
|
||||
content2 = (empackage(z, 'sshuttle') +
|
||||
@ -96,11 +103,21 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
empackage(z, 'sshuttle.server') +
|
||||
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"""
|
||||
import sys, os;
|
||||
verbosity=%d;
|
||||
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))
|
||||
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
||||
|
||||
@ -113,15 +130,77 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
sshl = shlex.split(ssh_cmd)
|
||||
else:
|
||||
sshl = ['ssh']
|
||||
if port is not None:
|
||||
portl = ["-p", str(port)]
|
||||
else:
|
||||
portl = []
|
||||
if python:
|
||||
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
||||
else:
|
||||
pycmd = ("P=python3; $P -V 2>/dev/null || P=python; "
|
||||
"exec \"$P\" -c %s") % quote(pyscript)
|
||||
pycmd = ("exec /bin/sh -c %s" % quote(pycmd))
|
||||
# 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; "
|
||||
"exec \"$P\" -c %s; exit 97") % \
|
||||
(os.devnull, quote(pyscript))
|
||||
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
|
||||
|
||||
if password is not None:
|
||||
os.environ['SSHPASS'] = str(password)
|
||||
argv = (["sshpass", "-e"] + sshl +
|
||||
portl +
|
||||
[rhost, '--', pycmd])
|
||||
|
||||
else:
|
||||
argv = (sshl +
|
||||
portl +
|
||||
[rhost, '--', pycmd])
|
||||
|
||||
# Our which() function searches for programs in get_path()
|
||||
# directories (which include PATH). This step isn't strictly
|
||||
# necessary if ssh is already in the user's PATH, but it makes the
|
||||
# error message friendlier if the user incorrectly passes in a
|
||||
# custom ssh command that we cannot find.
|
||||
abs_path = which(argv[0])
|
||||
if abs_path is None:
|
||||
raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path()))
|
||||
argv[0] = abs_path
|
||||
|
||||
(s1, s2) = socket.socketpair()
|
||||
|
||||
def setup():
|
||||
@ -129,7 +208,8 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||
s2.close()
|
||||
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
|
||||
s1.close()
|
||||
debug2('executing: %r\n' % argv)
|
||||
|
||||
debug2('executing: %r' % argv)
|
||||
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
|
||||
close_fds=True, stderr=stderr)
|
||||
os.close(s1a)
|
||||
|
@ -4,19 +4,19 @@ import socket
|
||||
import errno
|
||||
import select
|
||||
import os
|
||||
from sshuttle.helpers import b, binary_type, log, debug1, debug2, debug3, Fatal
|
||||
import fcntl
|
||||
|
||||
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
|
||||
|
||||
MAX_CHANNEL = 65535
|
||||
LATENCY_BUFFER_SIZE = 32768
|
||||
|
||||
# these don't exist in the socket module in python 2.3!
|
||||
SHUT_RD = 0
|
||||
SHUT_WR = 1
|
||||
SHUT_RDWR = 2
|
||||
|
||||
|
||||
HDR_LEN = 8
|
||||
|
||||
|
||||
CMD_EXIT = 0x4200
|
||||
CMD_PING = 0x4201
|
||||
CMD_PONG = 0x4202
|
||||
@ -55,17 +55,18 @@ cmd_to_name = {
|
||||
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||
errno.EHOSTDOWN, errno.ENETDOWN,
|
||||
errno.ENETUNREACH]
|
||||
errno.ENETUNREACH, errno.ECONNABORTED,
|
||||
errno.ECONNRESET]
|
||||
|
||||
|
||||
def _add(l, elem):
|
||||
if elem not in l:
|
||||
l.append(elem)
|
||||
def _add(socks, elem):
|
||||
if elem not in socks:
|
||||
socks.append(elem)
|
||||
|
||||
|
||||
def _fds(l):
|
||||
def _fds(socks):
|
||||
out = []
|
||||
for i in l:
|
||||
for i in socks:
|
||||
try:
|
||||
out.append(i.fileno())
|
||||
except AttributeError:
|
||||
@ -82,7 +83,7 @@ def _nb_clean(func, *args):
|
||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
raise
|
||||
else:
|
||||
debug3('%s: err was: %s\n' % (func.__name__, e))
|
||||
debug3('%s: err was: %s' % (func.__name__, e))
|
||||
return None
|
||||
|
||||
|
||||
@ -93,8 +94,12 @@ def _try_peername(sock):
|
||||
return '%s:%s' % (pn[0], pn[1])
|
||||
except socket.error:
|
||||
_, e = sys.exc_info()[:2]
|
||||
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
||||
if e.args[0] == errno.EINVAL:
|
||||
pass
|
||||
elif e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
||||
raise
|
||||
except AttributeError:
|
||||
pass
|
||||
return 'unknown'
|
||||
|
||||
|
||||
@ -106,7 +111,7 @@ class SockWrapper:
|
||||
def __init__(self, rsock, wsock, connect_to=None, peername=None):
|
||||
global _swcount
|
||||
_swcount += 1
|
||||
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
|
||||
debug3('creating new SockWrapper (%d now exist)' % _swcount)
|
||||
self.exc = None
|
||||
self.rsock = rsock
|
||||
self.wsock = wsock
|
||||
@ -119,9 +124,9 @@ class SockWrapper:
|
||||
def __del__(self):
|
||||
global _swcount
|
||||
_swcount -= 1
|
||||
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
|
||||
debug1('%r: deleting (%d remain)' % (self, _swcount))
|
||||
if self.exc:
|
||||
debug1('%r: error was: %s\n' % (self, self.exc))
|
||||
debug1('%r: error was: %s' % (self, self.exc))
|
||||
|
||||
def __repr__(self):
|
||||
if self.rsock == self.wsock:
|
||||
@ -143,14 +148,14 @@ class SockWrapper:
|
||||
if not self.connect_to:
|
||||
return # already connected
|
||||
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:
|
||||
self.rsock.connect(self.connect_to)
|
||||
# connected successfully (Linux)
|
||||
self.connect_to = None
|
||||
except socket.error:
|
||||
_, 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:
|
||||
# this is what happens when you call connect() on a socket
|
||||
# that is now connected but returned EINPROGRESS last time,
|
||||
@ -160,7 +165,7 @@ class SockWrapper:
|
||||
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_ERROR)
|
||||
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]:
|
||||
pass # not connected yet
|
||||
elif e.args[0] == 0:
|
||||
@ -186,13 +191,13 @@ class SockWrapper:
|
||||
|
||||
def noread(self):
|
||||
if not self.shut_read:
|
||||
debug2('%r: done reading\n' % self)
|
||||
debug2('%r: done reading' % self)
|
||||
self.shut_read = True
|
||||
# self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
|
||||
|
||||
def nowrite(self):
|
||||
if not self.shut_write:
|
||||
debug2('%r: done writing\n' % self)
|
||||
debug2('%r: done writing' % self)
|
||||
self.shut_write = True
|
||||
try:
|
||||
self.wsock.shutdown(SHUT_WR)
|
||||
@ -213,7 +218,7 @@ class SockWrapper:
|
||||
except OSError:
|
||||
_, e = sys.exc_info()[:2]
|
||||
if e.errno == errno.EPIPE:
|
||||
debug1('%r: uwrite: got EPIPE\n' % self)
|
||||
debug1('%r: uwrite: got EPIPE' % self)
|
||||
self.nowrite()
|
||||
return 0
|
||||
else:
|
||||
@ -270,12 +275,12 @@ class Handler:
|
||||
_add(r, i)
|
||||
|
||||
def callback(self, sock):
|
||||
log('--no callback defined-- %r\n' % self)
|
||||
log('--no callback defined-- %r' % self)
|
||||
(r, _, _) = select.select(self.socks, [], [], 0)
|
||||
for s in r:
|
||||
v = s.recv(4096)
|
||||
if not v:
|
||||
log('--closed-- %r\n' % self)
|
||||
log('--closed-- %r' % self)
|
||||
self.socks = []
|
||||
self.ok = False
|
||||
|
||||
@ -332,10 +337,10 @@ class Proxy(Handler):
|
||||
|
||||
class Mux(Handler):
|
||||
|
||||
def __init__(self, rsock, wsock):
|
||||
Handler.__init__(self, [rsock, wsock])
|
||||
self.rsock = rsock
|
||||
self.wsock = wsock
|
||||
def __init__(self, rfile, wfile):
|
||||
Handler.__init__(self, [rfile, wfile])
|
||||
self.rfile = rfile
|
||||
self.wfile = wfile
|
||||
self.new_channel = self.got_dns_req = self.got_routes = None
|
||||
self.got_udp_open = self.got_udp_data = self.got_udp_close = None
|
||||
self.got_host_req = self.got_host_list = None
|
||||
@ -364,7 +369,7 @@ class Mux(Handler):
|
||||
return total
|
||||
|
||||
def check_fullness(self):
|
||||
if self.fullness > 32768:
|
||||
if self.fullness > LATENCY_BUFFER_SIZE:
|
||||
if not self.too_full:
|
||||
self.send(0, CMD_PING, b('rttest'))
|
||||
self.too_full = True
|
||||
@ -372,26 +377,26 @@ class Mux(Handler):
|
||||
# for b in self.outbuf:
|
||||
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
|
||||
# ob.append(c)
|
||||
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
||||
# log('outbuf: %d %r' % (self.amount_queued(), ob))
|
||||
|
||||
def send(self, channel, cmd, data):
|
||||
assert isinstance(data, binary_type)
|
||||
assert isinstance(data, bytes)
|
||||
assert len(data) <= 65535
|
||||
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
|
||||
+ data
|
||||
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)),
|
||||
len(data), self.fullness))
|
||||
self.fullness += len(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)))
|
||||
if cmd == CMD_PING:
|
||||
self.send(0, CMD_PONG, data)
|
||||
elif cmd == CMD_PONG:
|
||||
debug2('received PING response\n')
|
||||
debug2('received PING response')
|
||||
self.too_full = False
|
||||
self.fullness = 0
|
||||
elif cmd == CMD_EXIT:
|
||||
@ -426,29 +431,44 @@ class Mux(Handler):
|
||||
else:
|
||||
callback = self.channels.get(channel)
|
||||
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)))
|
||||
else:
|
||||
callback(cmd, data)
|
||||
|
||||
def flush(self):
|
||||
self.wsock.setblocking(False)
|
||||
try:
|
||||
os.set_blocking(self.wfile.fileno(), False)
|
||||
except AttributeError:
|
||||
# python < 3.5
|
||||
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL)
|
||||
flags |= os.O_NONBLOCK
|
||||
flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags)
|
||||
if self.outbuf and self.outbuf[0]:
|
||||
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
|
||||
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
|
||||
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
|
||||
debug2('mux wrote: %r/%d' % (wrote, len(self.outbuf[0])))
|
||||
if wrote:
|
||||
self.outbuf[0] = self.outbuf[0][wrote:]
|
||||
while self.outbuf and not self.outbuf[0]:
|
||||
self.outbuf[0:1] = []
|
||||
|
||||
def fill(self):
|
||||
self.rsock.setblocking(False)
|
||||
try:
|
||||
read = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
||||
os.set_blocking(self.rfile.fileno(), False)
|
||||
except AttributeError:
|
||||
# python < 3.5
|
||||
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL)
|
||||
flags |= os.O_NONBLOCK
|
||||
flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
|
||||
try:
|
||||
# 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:
|
||||
_, e = sys.exc_info()[:2]
|
||||
raise Fatal('other end: %r' % e)
|
||||
# log('<<< %r\n' % b)
|
||||
# log('<<< %r' % b)
|
||||
if read == b(''): # EOF
|
||||
self.ok = False
|
||||
if read:
|
||||
@ -456,7 +476,7 @@ class Mux(Handler):
|
||||
|
||||
def handle(self):
|
||||
self.fill()
|
||||
# log('inbuf is: (%d,%d) %r\n'
|
||||
# log('inbuf is: (%d,%d) %r'
|
||||
# % (self.want, len(self.inbuf), self.inbuf))
|
||||
while 1:
|
||||
if len(self.inbuf) >= (self.want or HDR_LEN):
|
||||
@ -474,27 +494,27 @@ class Mux(Handler):
|
||||
break
|
||||
|
||||
def pre_select(self, r, w, x):
|
||||
_add(r, self.rsock)
|
||||
_add(r, self.rfile)
|
||||
if self.outbuf:
|
||||
_add(w, self.wsock)
|
||||
_add(w, self.wfile)
|
||||
|
||||
def callback(self, sock):
|
||||
(r, w, _) = select.select([self.rsock], [self.wsock], [], 0)
|
||||
if self.rsock in r:
|
||||
(r, w, _) = select.select([self.rfile], [self.wfile], [], 0)
|
||||
if self.rfile in r:
|
||||
self.handle()
|
||||
if self.outbuf and self.wsock in w:
|
||||
if self.outbuf and self.wfile in w:
|
||||
self.flush()
|
||||
|
||||
|
||||
class MuxWrapper(SockWrapper):
|
||||
|
||||
def __init__(self, mux, channel):
|
||||
SockWrapper.__init__(self, mux.rsock, mux.wsock)
|
||||
SockWrapper.__init__(self, mux.rfile, mux.wfile)
|
||||
self.mux = mux
|
||||
self.channel = channel
|
||||
self.mux.channels[channel] = self.got_packet
|
||||
self.socks = []
|
||||
debug2('new channel: %d\n' % channel)
|
||||
debug2('new channel: %d' % channel)
|
||||
|
||||
def __del__(self):
|
||||
self.nowrite()
|
||||
@ -510,7 +530,7 @@ class MuxWrapper(SockWrapper):
|
||||
|
||||
def setnoread(self):
|
||||
if not self.shut_read:
|
||||
debug2('%r: done reading\n' % self)
|
||||
debug2('%r: done reading' % self)
|
||||
self.shut_read = True
|
||||
self.maybe_close()
|
||||
|
||||
@ -521,13 +541,13 @@ class MuxWrapper(SockWrapper):
|
||||
|
||||
def setnowrite(self):
|
||||
if not self.shut_write:
|
||||
debug2('%r: done writing\n' % self)
|
||||
debug2('%r: done writing' % self)
|
||||
self.shut_write = True
|
||||
self.maybe_close()
|
||||
|
||||
def maybe_close(self):
|
||||
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
|
||||
# will then be able to reap our object.
|
||||
self.mux.channels[self.channel] = None
|
||||
@ -564,9 +584,9 @@ class MuxWrapper(SockWrapper):
|
||||
|
||||
|
||||
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.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||
|
||||
return SockWrapper(outsock, outsock,
|
||||
connect_to=(ip, port),
|
||||
peername='%s:%d' % (ip, port))
|
||||
@ -582,11 +602,11 @@ def runonce(handlers, mux):
|
||||
|
||||
for s in handlers:
|
||||
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),
|
||||
mux.fullness, mux.too_full))
|
||||
(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)))
|
||||
ready = r + w + x
|
||||
did = {}
|
||||
|
@ -8,12 +8,24 @@ _p = None
|
||||
|
||||
def start_syslog():
|
||||
global _p
|
||||
_p = ssubprocess.Popen(['logger',
|
||||
'-p', 'daemon.notice',
|
||||
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
_p = ssubprocess.Popen(
|
||||
['logger', '-p', 'daemon.notice', '-t', 'sshuttle'],
|
||||
stdin=ssubprocess.PIPE,
|
||||
stdout=devnull,
|
||||
stderr=devnull
|
||||
)
|
||||
|
||||
|
||||
def close_stdin():
|
||||
sys.stdin.close()
|
||||
|
||||
|
||||
def stdout_to_syslog():
|
||||
sys.stdout.flush()
|
||||
os.dup2(_p.stdin.fileno(), sys.stdout.fileno())
|
||||
|
||||
|
||||
def stderr_to_syslog():
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os.dup2(_p.stdin.fileno(), 2)
|
||||
os.dup2(_p.stdin.fileno(), sys.stderr.fileno())
|
||||
|
71
sshuttle/sudoers.py
Normal file
71
sshuttle/sudoers.py
Normal file
@ -0,0 +1,71 @@
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
from uuid import uuid4
|
||||
from subprocess import Popen, PIPE
|
||||
from sshuttle.helpers import log, debug1
|
||||
from distutils import spawn
|
||||
|
||||
path_to_sshuttle = sys.argv[0]
|
||||
path_to_dist_packages = os.path.dirname(os.path.abspath(__file__))[:-9]
|
||||
|
||||
# randomize command alias to avoid collisions
|
||||
command_alias = 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()}
|
||||
|
||||
# Template for the sudoers file
|
||||
template = '''
|
||||
Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
|
||||
|
||||
%(user_name)s ALL=NOPASSWD: %(ca)s
|
||||
'''
|
||||
|
||||
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):
|
||||
content = warning_msg
|
||||
content += template % {
|
||||
'ca': command_alias,
|
||||
'dist_packages': path_to_dist_packages,
|
||||
'py': sys.executable,
|
||||
'path': path_to_sshuttle,
|
||||
'user_name': user_name,
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def save_config(content, file_name):
|
||||
process = Popen([
|
||||
'/usr/bin/sudo',
|
||||
spawn.find_executable('sudoers-add'),
|
||||
file_name,
|
||||
], stdout=PIPE, stdin=PIPE)
|
||||
|
||||
process.stdin.write(content.encode())
|
||||
|
||||
streamdata = process.communicate()[0]
|
||||
sys.stdout.write(streamdata.decode("ASCII"))
|
||||
returncode = process.returncode
|
||||
|
||||
if returncode:
|
||||
log('Failed updating sudoers file.')
|
||||
debug1(streamdata)
|
||||
exit(returncode)
|
||||
else:
|
||||
log('Success, sudoers file update.')
|
||||
exit(0)
|
||||
|
||||
|
||||
def sudoers(user_name=None, no_modify=None, file_name=None):
|
||||
user_name = user_name or getpass.getuser()
|
||||
content = build_config(user_name)
|
||||
|
||||
if no_modify:
|
||||
sys.stdout.write(content)
|
||||
exit(0)
|
||||
else:
|
||||
sys.stdout.write(warning_msg)
|
||||
save_config(content, file_name)
|
@ -1,7 +1,7 @@
|
||||
from mock import Mock, patch, call
|
||||
import io
|
||||
from socket import AF_INET, AF_INET6
|
||||
|
||||
from unittest.mock import Mock, patch, call
|
||||
import sshuttle.firewall
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ NSLIST
|
||||
{inet},1.2.3.33
|
||||
{inet6},2404:6800:4004:80c::33
|
||||
PORTS 1024,1025,1026,1027
|
||||
GO 1 -
|
||||
GO 1 - 0x01 12345
|
||||
HOST 1.2.3.3,existing
|
||||
""".format(inet=AF_INET, inet6=AF_INET6))
|
||||
stdout = Mock()
|
||||
@ -55,7 +55,7 @@ def test_rewrite_etc_hosts(tmpdir):
|
||||
assert line == ""
|
||||
|
||||
with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
|
||||
sshuttle.firewall.restore_etc_hosts(10)
|
||||
sshuttle.firewall.restore_etc_hosts(hostmap, 10)
|
||||
assert orig_hosts.computehash() == new_hosts.computehash()
|
||||
|
||||
|
||||
@ -85,8 +85,9 @@ def test_subnet_weight():
|
||||
(AF_INET, 0, 1, '0.0.0.0', 0, 0)
|
||||
]
|
||||
|
||||
assert subnets_sorted == \
|
||||
sorted(subnets, key=sshuttle.firewall.subnet_weight, reverse=True)
|
||||
assert subnets_sorted == sorted(subnets,
|
||||
key=sshuttle.firewall.subnet_weight,
|
||||
reverse=True)
|
||||
|
||||
|
||||
@patch('sshuttle.firewall.rewrite_etc_hosts')
|
||||
@ -115,6 +116,8 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
|
||||
assert mock_setup_daemon.mock_calls == [call()]
|
||||
assert mock_get_method.mock_calls == [
|
||||
call('not_auto'),
|
||||
call().is_supported(),
|
||||
call().is_supported().__bool__(),
|
||||
call().setup_firewall(
|
||||
1024, 1026,
|
||||
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||
@ -122,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, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
||||
True,
|
||||
None),
|
||||
None,
|
||||
'0x01'),
|
||||
call().setup_firewall(
|
||||
1025, 1027,
|
||||
[(AF_INET, u'1.2.3.33')],
|
||||
@ -130,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, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||
True,
|
||||
None),
|
||||
None,
|
||||
'0x01'),
|
||||
call().restore_firewall(1024, AF_INET6, True, None),
|
||||
call().restore_firewall(1025, AF_INET, True, None),
|
||||
]
|
@ -1,10 +1,9 @@
|
||||
from mock import patch, call
|
||||
import sys
|
||||
import io
|
||||
import socket
|
||||
from socket import AF_INET, AF_INET6
|
||||
import errno
|
||||
|
||||
from unittest.mock import patch, call
|
||||
import sshuttle.helpers
|
||||
|
||||
|
||||
@ -25,19 +24,19 @@ def test_log(mock_stderr, mock_stdout):
|
||||
call.flush(),
|
||||
]
|
||||
assert mock_stderr.mock_calls == [
|
||||
call.write('prefix: message'),
|
||||
call.write('prefix: message\r\n'),
|
||||
call.flush(),
|
||||
call.write('prefix: abc'),
|
||||
call.write('prefix: abc\r\n'),
|
||||
call.flush(),
|
||||
call.write('prefix: message 1\n'),
|
||||
call.write('prefix: message 1\r\n'),
|
||||
call.flush(),
|
||||
call.write('prefix: message 2\n'),
|
||||
call.write('---> line2\n'),
|
||||
call.write('---> line3\n'),
|
||||
call.write('prefix: message 2\r\n'),
|
||||
call.write(' line2\r\n'),
|
||||
call.write(' line3\r\n'),
|
||||
call.flush(),
|
||||
call.write('prefix: message 3\n'),
|
||||
call.write('---> line2\n'),
|
||||
call.write('---> line3\n'),
|
||||
call.write('prefix: message 3\r\n'),
|
||||
call.write(' line2\r\n'),
|
||||
call.write(' line3\r\n'),
|
||||
call.flush(),
|
||||
]
|
||||
|
||||
@ -52,7 +51,7 @@ def test_debug1(mock_stderr, mock_stdout):
|
||||
call.flush(),
|
||||
]
|
||||
assert mock_stderr.mock_calls == [
|
||||
call.write('prefix: message'),
|
||||
call.write('prefix: message\r\n'),
|
||||
call.flush(),
|
||||
]
|
||||
|
||||
@ -77,7 +76,7 @@ def test_debug2(mock_stderr, mock_stdout):
|
||||
call.flush(),
|
||||
]
|
||||
assert mock_stderr.mock_calls == [
|
||||
call.write('prefix: message'),
|
||||
call.write('prefix: message\r\n'),
|
||||
call.flush(),
|
||||
]
|
||||
|
||||
@ -102,7 +101,7 @@ def test_debug3(mock_stderr, mock_stdout):
|
||||
call.flush(),
|
||||
]
|
||||
assert mock_stderr.mock_calls == [
|
||||
call.write('prefix: message'),
|
||||
call.write('prefix: message\r\n'),
|
||||
call.flush(),
|
||||
]
|
||||
|
||||
@ -132,7 +131,7 @@ nameserver 2404:6800:4004:80c::3
|
||||
nameserver 2404:6800:4004:80c::4
|
||||
""")
|
||||
|
||||
ns = sshuttle.helpers.resolvconf_nameservers()
|
||||
ns = sshuttle.helpers.resolvconf_nameservers(False)
|
||||
assert ns == [
|
||||
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
||||
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
||||
@ -157,7 +156,7 @@ nameserver 2404:6800:4004:80c::2
|
||||
nameserver 2404:6800:4004:80c::3
|
||||
nameserver 2404:6800:4004:80c::4
|
||||
""")
|
||||
ns = sshuttle.helpers.resolvconf_random_nameserver()
|
||||
ns = sshuttle.helpers.resolvconf_random_nameserver(False)
|
||||
assert ns in [
|
||||
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
||||
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
||||
@ -193,9 +192,5 @@ def test_family_ip_tuple():
|
||||
def test_family_to_string():
|
||||
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
|
||||
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
|
||||
if sys.version_info < (3, 0):
|
||||
expected = "1"
|
||||
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1"
|
||||
else:
|
||||
expected = 'AddressFamily.AF_UNIX'
|
||||
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected
|
@ -1,9 +1,9 @@
|
||||
import pytest
|
||||
from mock import Mock, patch, call
|
||||
import socket
|
||||
from socket import AF_INET, AF_INET6
|
||||
import struct
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, call
|
||||
from sshuttle.helpers import Fatal
|
||||
from sshuttle.methods import get_method
|
||||
|
||||
@ -11,19 +11,29 @@ from sshuttle.methods import get_method
|
||||
def test_get_supported_features():
|
||||
method = get_method('nat')
|
||||
features = method.get_supported_features()
|
||||
assert not features.ipv6
|
||||
assert features.ipv6
|
||||
assert not features.udp
|
||||
assert features.dns
|
||||
|
||||
|
||||
def test_get_tcp_dstip():
|
||||
sock = Mock()
|
||||
sock.family = AF_INET
|
||||
sock.getsockopt.return_value = struct.pack(
|
||||
'!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1)
|
||||
method = get_method('nat')
|
||||
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
||||
assert sock.mock_calls == [call.getsockopt(0, 80, 16)]
|
||||
|
||||
sock = Mock()
|
||||
sock.family = AF_INET6
|
||||
sock.getsockopt.return_value = struct.pack(
|
||||
'!HH4xBBBBBBBBBBBBBBBB', socket.ntohs(AF_INET6),
|
||||
1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)
|
||||
method = get_method('nft')
|
||||
assert method.get_tcp_dstip(sock) == ('::1', 1024)
|
||||
assert sock.mock_calls == [call.getsockopt(41, 80, 64)]
|
||||
|
||||
|
||||
def test_recv_udp():
|
||||
sock = Mock()
|
||||
@ -75,26 +85,52 @@ def test_firewall_command():
|
||||
|
||||
|
||||
@patch('sshuttle.methods.nat.ipt')
|
||||
@patch('sshuttle.methods.nat.ipt_ttl')
|
||||
@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
|
||||
method = get_method('nat')
|
||||
assert method.name == 'nat'
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
assert mock_ipt_chain_exists.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)],
|
||||
True,
|
||||
None)
|
||||
assert str(excinfo.value) \
|
||||
== 'Address family "AF_INET6" unsupported by nat method_name'
|
||||
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_ttl.mock_calls == []
|
||||
assert mock_ipt.mock_calls == []
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
@ -105,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, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||
True,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert str(excinfo.value) == 'UDP not supported by nat method_name'
|
||||
assert mock_ipt_chain_exists.mock_calls == []
|
||||
assert mock_ipt_ttl.mock_calls == []
|
||||
assert mock_ipt.mock_calls == []
|
||||
|
||||
method.setup_firewall(
|
||||
@ -118,18 +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, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||
False,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert mock_ipt_chain_exists.mock_calls == [
|
||||
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||
]
|
||||
assert mock_ipt_ttl.mock_calls == [
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
|
||||
'--to-ports', '1025'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||
'--dest', u'1.2.3.33/32', '-p', 'udp',
|
||||
'--dport', '53', '--to-ports', '1027')
|
||||
]
|
||||
assert mock_ipt.mock_calls == [
|
||||
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||
@ -139,24 +168,45 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||
call(AF_INET, 'nat', '-F', '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', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||
'--dest', u'1.2.3.33', '-p', 'udp',
|
||||
'--dport', '53', '--to-ports', '1027'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
|
||||
'-m', 'addrtype', '--dst-type', 'LOCAL'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080'),
|
||||
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
|
||||
'--to-ports', '1025')
|
||||
]
|
||||
mock_ipt_chain_exists.reset_mock()
|
||||
mock_ipt_ttl.reset_mock()
|
||||
mock_ipt.reset_mock()
|
||||
|
||||
method.restore_firewall(1025, AF_INET, False, None)
|
||||
assert mock_ipt_chain_exists.mock_calls == [
|
||||
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||
]
|
||||
assert mock_ipt_ttl.mock_calls == []
|
||||
assert mock_ipt.mock_calls == [
|
||||
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||
call(AF_INET, 'nat', '-D', 'PREROUTING', '-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', '-F', 'sshuttle-1025'),
|
||||
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
|
||||
]
|
||||
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()
|
@ -1,10 +1,10 @@
|
||||
import pytest
|
||||
from mock import Mock, patch, call, ANY
|
||||
import socket
|
||||
from socket import AF_INET, AF_INET6
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, call, ANY
|
||||
from sshuttle.methods import get_method
|
||||
from sshuttle.helpers import Fatal
|
||||
from sshuttle.helpers import Fatal, get_env
|
||||
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, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||
False,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert mock_ioctl.mock_calls == [
|
||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||
@ -205,10 +206,10 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n'
|
||||
b'rdr pass on lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 -> ::1 port 1026\n'
|
||||
b'pass out quick inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
|
||||
b'pass out inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-E'),
|
||||
@ -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, 32, True, u'1.2.3.66', 80, 80)],
|
||||
True,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||
assert mock_pf_get_dev.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, 32, True, u'1.2.3.66', 80, 80)],
|
||||
False,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert mock_ioctl.mock_calls == [
|
||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||
@ -257,8 +260,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'-> 127.0.0.1 port 1025\n'
|
||||
b'rdr pass on lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
||||
b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
|
||||
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-E'),
|
||||
@ -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, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||
False,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
|
||||
assert mock_pfctl.mock_calls == [
|
||||
call('-s all'),
|
||||
@ -308,15 +312,16 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n'
|
||||
b'rdr pass on lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 -> ::1 port 1026\n'
|
||||
b'pass out quick inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
|
||||
b'pass out inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out route-to lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-e'),
|
||||
]
|
||||
assert call(['kldload', 'pf']) in mock_subprocess_call.mock_calls
|
||||
assert call(['kldload', 'pf'], env=get_env()) in \
|
||||
mock_subprocess_call.mock_calls
|
||||
mock_pf_get_dev.reset_mock()
|
||||
mock_ioctl.reset_mock()
|
||||
mock_pfctl.reset_mock()
|
||||
@ -329,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, 32, True, u'1.2.3.66', 80, 80)],
|
||||
True,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||
assert mock_pf_get_dev.mock_calls == []
|
||||
assert mock_ioctl.mock_calls == []
|
||||
@ -342,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, 32, True, u'1.2.3.66', 80, 80)],
|
||||
False,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert mock_ioctl.mock_calls == [
|
||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
||||
@ -359,8 +366,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
||||
b'to 1.2.3.0/24 -> 127.0.0.1 port 1025\n'
|
||||
b'rdr pass on lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
||||
b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
|
||||
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out route-to lo0 inet proto udp '
|
||||
b'to <dns_servers> port 53 keep state\n'),
|
||||
call('-e'),
|
||||
@ -400,11 +407,12 @@ 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, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||
False,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
|
||||
assert mock_ioctl.mock_calls == [
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
]
|
||||
assert mock_pfctl.mock_calls == [
|
||||
call('-s Interfaces -i lo -v'),
|
||||
@ -416,10 +424,10 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'port 8000:9000 divert-to ::1 port 1024\n'
|
||||
b'pass in on lo0 inet6 proto udp '
|
||||
b'to <dns_servers> port 53 rdr-to ::1 port 1026\n'
|
||||
b'pass out quick inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out inet6 proto tcp to 2404:6800:4004:80c::/64 '
|
||||
b'port 8000:9000 route-to lo0 keep state\n'
|
||||
b'pass out inet6 proto tcp to '
|
||||
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
|
||||
b'pass out inet6 proto udp to '
|
||||
b'<dns_servers> port 53 route-to lo0 keep state\n'),
|
||||
call('-e'),
|
||||
@ -436,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, 32, True, u'1.2.3.66', 80, 80)],
|
||||
True,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||
assert mock_pf_get_dev.mock_calls == []
|
||||
assert mock_ioctl.mock_calls == []
|
||||
@ -449,10 +458,11 @@ 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, 32, True, u'1.2.3.66', 80, 80)],
|
||||
False,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert mock_ioctl.mock_calls == [
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd48441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
call(mock_pf_get_dev(), 0xcd60441a, ANY),
|
||||
]
|
||||
assert mock_pfctl.mock_calls == [
|
||||
call('-s Interfaces -i lo -v'),
|
||||
@ -464,8 +474,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||
b'127.0.0.1 port 1025\n'
|
||||
b'pass in on lo0 inet proto udp to '
|
||||
b'<dns_servers> port 53 rdr-to 127.0.0.1 port 1027\n'
|
||||
b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out inet proto tcp to 1.2.3.0/24 route-to lo0 keep state\n'
|
||||
b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n'
|
||||
b'pass out inet proto udp to '
|
||||
b'<dns_servers> port 53 route-to lo0 keep state\n'),
|
||||
call('-e'),
|
@ -1,13 +1,12 @@
|
||||
import socket
|
||||
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
|
||||
|
||||
|
||||
@patch("sshuttle.methods.tproxy.recvmsg")
|
||||
def test_get_supported_features_recvmsg(mock_recvmsg):
|
||||
def test_get_supported_features():
|
||||
method = get_method('tproxy')
|
||||
features = method.get_supported_features()
|
||||
assert features.ipv6
|
||||
@ -15,15 +14,6 @@ def test_get_supported_features_recvmsg(mock_recvmsg):
|
||||
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():
|
||||
sock = Mock()
|
||||
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_ttl')
|
||||
@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
|
||||
method = get_method('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, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
|
||||
True,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert mock_ipt_chain_exists.mock_calls == [
|
||||
call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
|
||||
call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
|
||||
call(AF_INET6, 'mangle', 'sshuttle-d-1024')
|
||||
]
|
||||
assert mock_ipt_ttl.mock_calls == []
|
||||
assert mock_ipt.mock_calls == [
|
||||
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', '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', 'PREROUTING', '1', '-j',
|
||||
'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',
|
||||
'--set-mark', '1'),
|
||||
'--set-mark', '0x01'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
|
||||
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::33/32',
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1',
|
||||
'--tproxy-mark', '0x01',
|
||||
'--dest', u'2404:6800:4004:80c::33/32',
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-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',
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
|
||||
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'),
|
||||
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',
|
||||
'--on-port', '1024'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
||||
'-m', 'udp', '-p', 'udp'),
|
||||
'--set-mark', '0x01', '--dest', u'2404:6800:4004:80c::/64',
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000'),
|
||||
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
||||
'--tproxy-mark', '0x01', '--dest',
|
||||
u'2404:6800:4004:80c::/64',
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
|
||||
'--on-port', '1024')
|
||||
]
|
||||
mock_ipt_chain_exists.reset_mock()
|
||||
mock_ipt_ttl.reset_mock()
|
||||
mock_ipt.reset_mock()
|
||||
|
||||
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-d-1025')
|
||||
]
|
||||
assert mock_ipt_ttl.mock_calls == []
|
||||
assert mock_ipt.mock_calls == [
|
||||
call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', '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')
|
||||
]
|
||||
mock_ipt_chain_exists.reset_mock()
|
||||
mock_ipt_ttl.reset_mock()
|
||||
mock_ipt.reset_mock()
|
||||
|
||||
# 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, 32, True, u'1.2.3.66', 80, 80)],
|
||||
True,
|
||||
None)
|
||||
None,
|
||||
'0x01')
|
||||
assert mock_ipt_chain_exists.mock_calls == [
|
||||
call(AF_INET, 'mangle', 'sshuttle-m-1025'),
|
||||
call(AF_INET, 'mangle', 'sshuttle-t-1025'),
|
||||
call(AF_INET, 'mangle', 'sshuttle-d-1025')
|
||||
]
|
||||
assert mock_ipt_ttl.mock_calls == []
|
||||
assert mock_ipt.mock_calls == [
|
||||
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', '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', 'PREROUTING', '1', '-j',
|
||||
'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',
|
||||
'-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-t-1025', '-m', 'socket',
|
||||
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
|
||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
||||
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
|
||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||
'--set-mark', '1', '--dest', u'1.2.3.33/32',
|
||||
'--set-mark', '0x01', '--dest', u'1.2.3.33/32',
|
||||
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
||||
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||
'--tproxy-mark', '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'),
|
||||
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
||||
'--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',
|
||||
'--dport', '80:80'),
|
||||
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'),
|
||||
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'),
|
||||
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'),
|
||||
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')
|
||||
]
|
||||
mock_ipt_chain_exists.reset_mock()
|
||||
mock_ipt_ttl.reset_mock()
|
||||
mock_ipt.reset_mock()
|
||||
|
||||
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-d-1025')
|
||||
]
|
||||
assert mock_ipt_ttl.mock_calls == []
|
||||
assert mock_ipt.mock_calls == [
|
||||
call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', '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')
|
||||
]
|
||||
mock_ipt_chain_exists.reset_mock()
|
||||
mock_ipt_ttl.reset_mock()
|
||||
mock_ipt.reset_mock()
|
@ -1,8 +1,10 @@
|
||||
import socket
|
||||
import pytest
|
||||
import sshuttle.options
|
||||
from argparse import ArgumentTypeError as Fatal
|
||||
|
||||
import pytest
|
||||
|
||||
import sshuttle.options
|
||||
|
||||
_ip4_reprs = {
|
||||
'0.0.0.0': '0.0.0.0',
|
||||
'255.255.255.255': '255.255.255.255',
|
||||
@ -11,7 +13,6 @@ _ip4_reprs = {
|
||||
'3098282570': '184.172.10.74',
|
||||
'0xb8.0xac.0x0a.0x4a': '184.172.10.74',
|
||||
'0270.0254.0012.0112': '184.172.10.74',
|
||||
'localhost': '127.0.0.1'
|
||||
}
|
||||
|
||||
_ip4_swidths = (1, 8, 22, 27, 32)
|
||||
@ -25,10 +26,11 @@ _ip6_reprs = {
|
||||
|
||||
_ip6_swidths = (48, 64, 96, 115, 128)
|
||||
|
||||
|
||||
def test_parse_subnetport_ip4():
|
||||
for ip_repr, ip in _ip4_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport(ip_repr) \
|
||||
== (socket.AF_INET, ip, 32, 0, 0)
|
||||
== [(socket.AF_INET, ip, 32, 0, 0)]
|
||||
with pytest.raises(Fatal) as excinfo:
|
||||
sshuttle.options.parse_subnetport('10.256.0.0')
|
||||
assert str(excinfo.value) == 'Unable to resolve address: 10.256.0.0'
|
||||
@ -39,34 +41,35 @@ def test_parse_subnetport_ip4_with_mask():
|
||||
for swidth in _ip4_swidths:
|
||||
assert sshuttle.options.parse_subnetport(
|
||||
'/'.join((ip_repr, str(swidth)))
|
||||
) == (socket.AF_INET, ip, swidth, 0, 0)
|
||||
) == [(socket.AF_INET, ip, swidth, 0, 0)]
|
||||
assert sshuttle.options.parse_subnetport('0/0') \
|
||||
== (socket.AF_INET, '0.0.0.0', 0, 0, 0)
|
||||
== [(socket.AF_INET, '0.0.0.0', 0, 0, 0)]
|
||||
with pytest.raises(Fatal) as excinfo:
|
||||
sshuttle.options.parse_subnetport('10.0.0.0/33')
|
||||
assert str(excinfo.value) == 'width 33 is not between 0 and 32'
|
||||
assert str(excinfo.value) \
|
||||
== 'Slash in CIDR notation (/33) is not between 0 and 32'
|
||||
|
||||
|
||||
def test_parse_subnetport_ip4_with_port():
|
||||
for ip_repr, ip in _ip4_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \
|
||||
== (socket.AF_INET, ip, 32, 80, 80)
|
||||
== [(socket.AF_INET, ip, 32, 80, 80)]
|
||||
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\
|
||||
== (socket.AF_INET, ip, 32, 80, 90)
|
||||
== [(socket.AF_INET, ip, 32, 80, 90)]
|
||||
|
||||
|
||||
def test_parse_subnetport_ip4_with_mask_and_port():
|
||||
for ip_repr, ip in _ip4_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \
|
||||
== (socket.AF_INET, ip, 32, 80, 80)
|
||||
== [(socket.AF_INET, ip, 32, 80, 80)]
|
||||
assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \
|
||||
== (socket.AF_INET, ip, 16, 80, 90)
|
||||
== [(socket.AF_INET, ip, 16, 80, 90)]
|
||||
|
||||
|
||||
def test_parse_subnetport_ip6():
|
||||
for ip_repr, ip in _ip6_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport(ip_repr) \
|
||||
== (socket.AF_INET6, ip, 128, 0, 0)
|
||||
== [(socket.AF_INET6, ip, 128, 0, 0)]
|
||||
|
||||
|
||||
def test_parse_subnetport_ip6_with_mask():
|
||||
@ -74,25 +77,31 @@ def test_parse_subnetport_ip6_with_mask():
|
||||
for swidth in _ip4_swidths + _ip6_swidths:
|
||||
assert sshuttle.options.parse_subnetport(
|
||||
'/'.join((ip_repr, str(swidth)))
|
||||
) == (socket.AF_INET6, ip, swidth, 0, 0)
|
||||
) == [(socket.AF_INET6, ip, swidth, 0, 0)]
|
||||
assert sshuttle.options.parse_subnetport('::/0') \
|
||||
== (socket.AF_INET6, '::', 0, 0, 0)
|
||||
== [(socket.AF_INET6, '::', 0, 0, 0)]
|
||||
with pytest.raises(Fatal) as excinfo:
|
||||
sshuttle.options.parse_subnetport('fc00::/129')
|
||||
assert str(excinfo.value) == 'width 129 is not between 0 and 128'
|
||||
assert str(excinfo.value) \
|
||||
== 'Slash in CIDR notation (/129) is not between 0 and 128'
|
||||
|
||||
|
||||
def test_parse_subnetport_ip6_with_port():
|
||||
for ip_repr, ip in _ip6_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \
|
||||
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||
== [(socket.AF_INET6, ip, 128, 80, 80)]
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \
|
||||
== (socket.AF_INET6, ip, 128, 80, 90)
|
||||
== [(socket.AF_INET6, ip, 128, 80, 90)]
|
||||
|
||||
|
||||
def test_parse_subnetport_ip6_with_mask_and_port():
|
||||
for ip_repr, ip in _ip6_reprs.items():
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \
|
||||
== (socket.AF_INET6, ip, 128, 80, 80)
|
||||
== [(socket.AF_INET6, ip, 128, 80, 80)]
|
||||
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
|
||||
== (socket.AF_INET6, ip, 16, 80, 90)
|
||||
== [(socket.AF_INET6, ip, 16, 80, 90)]
|
||||
|
||||
|
||||
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,7 @@
|
||||
from mock import Mock, patch, call
|
||||
import socket
|
||||
|
||||
from unittest.mock import Mock, patch, call
|
||||
|
||||
import sshuttle.sdnotify
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import io
|
||||
import socket
|
||||
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
import sshuttle.server
|
||||
from mock import patch, Mock
|
||||
|
||||
|
||||
def test__ipmatch():
|
20
tests/ssh/test_parse_hostport.py
Normal file
20
tests/ssh/test_parse_hostport.py
Normal file
@ -0,0 +1,20 @@
|
||||
from sshuttle.ssh import parse_hostport
|
||||
|
||||
|
||||
def test_host_only():
|
||||
assert parse_hostport("host") == (None, None, None, "host")
|
||||
assert parse_hostport("1.2.3.4") == (None, None, None, "1.2.3.4")
|
||||
assert parse_hostport("2001::1") == (None, None, None, "2001::1")
|
||||
assert parse_hostport("[2001::1]") == (None, None, None, "2001::1")
|
||||
|
||||
|
||||
def test_host_and_port():
|
||||
assert parse_hostport("host:22") == (None, None, 22, "host")
|
||||
assert parse_hostport("1.2.3.4:22") == (None, None, 22, "1.2.3.4")
|
||||
assert parse_hostport("[2001::1]:22") == (None, None, 22, "2001::1")
|
||||
|
||||
|
||||
def test_username_and_host():
|
||||
assert parse_hostport("user@host") == ("user", None, None, "host")
|
||||
assert parse_hostport("user:@host") == ("user", None, None, "host")
|
||||
assert parse_hostport("user:pass@host") == ("user", "pass", None, "host")
|
22
tox.ini
22
tox.ini
@ -1,21 +1,23 @@
|
||||
[tox]
|
||||
downloadcache = {toxworkdir}/cache/
|
||||
envlist =
|
||||
py27,
|
||||
py34,
|
||||
py35,
|
||||
py36,
|
||||
py37,
|
||||
py38,
|
||||
py39,
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py26: python2.6
|
||||
py27: python2.7
|
||||
py34: python3.4
|
||||
py35: python3.5
|
||||
py36: python3.6
|
||||
py37: python3.7
|
||||
py38: python3.8
|
||||
py39: python3.9
|
||||
commands =
|
||||
flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
|
||||
flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
py.test
|
||||
pip install -e .
|
||||
# actual flake8 test
|
||||
flake8 sshuttle tests
|
||||
# flake8 complexity warnings
|
||||
flake8 sshuttle tests --exit-zero --max-complexity=10
|
||||
pytest
|
||||
deps =
|
||||
-rrequirements-tests.txt
|
||||
|
Reference in New Issue
Block a user