mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 08:40:30 +02:00
Compare commits
277 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
6ec42adbf4 | |||
2200d824bf | |||
9715a1d6f2 | |||
8bfc03b256 | |||
884bd6deb0 | |||
a215f1b227 | |||
11455d0bcd | |||
74acc10385 | |||
084bf5f0f2 | |||
1940b524f1 | |||
d11f5b9d16 | |||
93b969a049 | |||
f27b27b0e8 | |||
fc08fb4086 | |||
e82d5a8e7c | |||
d9d61e6ab2 | |||
179bb107e1 | |||
9a176aa96f | |||
6b48301b86 | |||
be90cc8abd | |||
512396e06b | |||
7a71ae1380 | |||
3a6f6cb795 | |||
81ab587698 | |||
817284c2f8 | |||
71d65f3831 | |||
9f238ebca8 | |||
9b315746d1 | |||
6a488b3db9 | |||
112931dd2c | |||
ad676029c7 | |||
47030e846b | |||
416636fa9b | |||
4300a02343 | |||
4e8c5411b5 | |||
6cdc4da1e4 | |||
8add00866c | |||
94ea0a3bed | |||
9b7ce2811e | |||
7726dea27c | |||
3635cc17ad | |||
ae13316e83 | |||
e173eb6016 | |||
29cd75b6f7 | |||
4c50be0bc7 | |||
2fa0cd06fb | |||
4d8b758d32 | |||
4e8c2b9c68 | |||
be559fc78b | |||
d2e97a60f7 | |||
cdbb379910 | |||
b65bb29023 | |||
c093b4bd96 | |||
e76d1e14bd | |||
6c6a39fefa | |||
714bd9f81b | |||
c746d6f7db | |||
f9361d7014 | |||
c4a41ada09 | |||
ef83a5c573 | |||
af9ebd0f4b | |||
9a9015a75e | |||
d7d24f956b | |||
809fad537f | |||
abce18cfc2 | |||
5e90491344 | |||
e8ceccc3d5 | |||
e39c4afce0 | |||
0e52cce9d1 | |||
6d5d0d766f | |||
08fb3be7a0 | |||
fee5868196 | |||
fbbcc05d58 | |||
15b394da86 | |||
0ed5ef9a97 | |||
c0c3612e6d | |||
0033efca11 | |||
ae6e25302f | |||
ffd95fb776 | |||
acb5aa5386 | |||
4801ae6627 | |||
f57ad356b9 | |||
a441a03e57 | |||
d2fdb6c029 | |||
2c20a1fd5a | |||
915f72de35 | |||
1ffc3f52a1 | |||
8520ea2787 | |||
6a394deaf2 | |||
83d5c59a57 | |||
1cfd9eb9d7 | |||
f8d58fa4f0 | |||
d2d5a37541 | |||
e9be2deea0 | |||
22b1b54bfd | |||
a43c668dde | |||
e0dfb95596 | |||
5d28ce8272 | |||
f876c5db5e | |||
2e1beefc9a | |||
5a20783baa | |||
495b3c39ea | |||
f3cbc5018a | |||
e73e797f33 | |||
1d64879613 | |||
8fad282bfd | |||
1dda9dd621 | |||
74e308a29f | |||
516ff7bc4a | |||
89c5b57019 | |||
a8b288338b | |||
6fd02933dd | |||
7ea2d973c7 | |||
8ec5d1a5ac | |||
4241381d82 | |||
6e15e69029 | |||
8fa45885cc | |||
b8160c4a37 | |||
05bacf6fd6 | |||
dea3f21943 | |||
d522d1e1bd | |||
3541e4bdfe | |||
efdb9b8f94 | |||
7875d1b97a | |||
2b0d0065c7 | |||
9e3f02c199 | |||
8bdefcd10d | |||
29b6e8301f | |||
083293ea0d | |||
2c07985924 | |||
756025b1bc | |||
cedc8dc146 | |||
e8047ce3a9 | |||
fae4cb1dbf | |||
7d8309ef05 | |||
b7d37e44fb | |||
4a954c547a | |||
4fcf7c73da | |||
ba8e948c0d | |||
e06f0240cb | |||
517fc2c930 | |||
11533869a8 | |||
0392a779a2 | |||
ee26157faa | |||
0bdfb883aa | |||
ff9756f290 | |||
59865269ac | |||
28017303f2 | |||
c5af6fef8c | |||
6835183b37 | |||
242c266e7d | |||
7408ab3c53 | |||
ea10ff1305 | |||
d9939b8460 | |||
80f363842d | |||
262affe94f | |||
d80b590a71 | |||
7f0b5c698b | |||
f59508f41b | |||
329867a090 | |||
1e82571b3d | |||
aab973f12e | |||
d982b36521 | |||
a90877b49b | |||
fc5545ccde | |||
629c386dc5 |
35
.github/workflows/pythonpackage.yml
vendored
Normal file
35
.github/workflows/pythonpackage.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: Python package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-tests.txt
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
flake8 sshuttle tests --count --show-source --statistics
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
PYTHONPATH=$PWD pytest
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,6 +1,17 @@
|
|||||||
|
/sshuttle/version.py
|
||||||
|
/tmp/
|
||||||
|
/.cache/
|
||||||
|
/.eggs/
|
||||||
|
/.tox/
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
/sshuttle.egg-info/
|
||||||
|
/docs/_build/
|
||||||
*.pyc
|
*.pyc
|
||||||
*~
|
*~
|
||||||
*.8
|
*.8
|
||||||
/.do_built
|
/.do_built
|
||||||
/.do_built.dir
|
/.do_built.dir
|
||||||
/.redo
|
/.redo
|
||||||
|
/.pytest_cache/
|
||||||
|
/.python-version
|
||||||
|
24
.prospector.yml
Normal file
24
.prospector.yml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
strictness: medium
|
||||||
|
|
||||||
|
pylint:
|
||||||
|
disable:
|
||||||
|
- too-many-statements
|
||||||
|
- too-many-locals
|
||||||
|
- too-many-function-args
|
||||||
|
- too-many-arguments
|
||||||
|
- too-many-branches
|
||||||
|
- bare-except
|
||||||
|
- protected-access
|
||||||
|
- no-else-return
|
||||||
|
- unused-argument
|
||||||
|
- method-hidden
|
||||||
|
- arguments-differ
|
||||||
|
- wrong-import-position
|
||||||
|
- raising-bad-type
|
||||||
|
|
||||||
|
pep8:
|
||||||
|
options:
|
||||||
|
max-line-length: 79
|
||||||
|
|
||||||
|
mccabe:
|
||||||
|
run: false
|
11
.travis.yml
11
.travis.yml
@ -1,11 +0,0 @@
|
|||||||
language: python
|
|
||||||
python:
|
|
||||||
- 2.7
|
|
||||||
- 3.5
|
|
||||||
- pypy
|
|
||||||
|
|
||||||
install:
|
|
||||||
- travis_retry pip install -q pytest mock
|
|
||||||
|
|
||||||
script:
|
|
||||||
- PYTHONPATH=. py.test
|
|
266
CHANGES.rst
266
CHANGES.rst
@ -1,11 +1,269 @@
|
|||||||
Release 0.75 (Jan 12, 2016)
|
==========
|
||||||
===========================
|
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`_.
|
||||||
|
|
||||||
|
.. _`Keep a Changelog`: http://keepachangelog.com/
|
||||||
|
.. _`Semantic Versioning`: http://semver.org/
|
||||||
|
|
||||||
|
|
||||||
|
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 replacmeent for sudo on OpenBSD.
|
||||||
|
* Added ChromeOS section to documentation (#262)
|
||||||
|
* Add --no-sudo-pythonpath option
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
~~~~~
|
||||||
|
* Fix forwarding to a single port.
|
||||||
|
* Various updates to documentation.
|
||||||
|
* Don't crash if we can't look up peername
|
||||||
|
* Fix missing string formatting argument
|
||||||
|
* Moved sshuttle/tests into tests.
|
||||||
|
* Updated bandit config.
|
||||||
|
* Replace path /dev/null by os.devnull.
|
||||||
|
* Added coverage report to tests.
|
||||||
|
* Fixes support for OpenBSD (6.1+) (#282).
|
||||||
|
* Close stdin, stdout, and stderr when using syslog or forking to daemon (#283).
|
||||||
|
* Changes pf exclusion rules precedence.
|
||||||
|
* Fix deadlock with iptables with large ruleset.
|
||||||
|
* docs: document --ns-hosts --to-ns and update --dns.
|
||||||
|
* Use subprocess.check_output instead of run.
|
||||||
|
* Fix potential deadlock condition in nft_get_handle.
|
||||||
|
* auto-nets: retrieve routes only if using auto-nets.
|
||||||
|
|
||||||
|
|
||||||
|
0.78.4 - 2018-04-02
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Added
|
||||||
|
~~~~~
|
||||||
|
* Add homebrew instructions.
|
||||||
|
* Route traffic by linux user.
|
||||||
|
* Add nat-like method using nftables instead of iptables.
|
||||||
|
|
||||||
|
Changed
|
||||||
|
~~~~~~~
|
||||||
|
* Talk to custom DNS server on pod, instead of the ones in /etc/resolv.conf.
|
||||||
|
* Add new option for overriding destination DNS server.
|
||||||
|
* Changed subnet parsing. Previously 10/8 become 10.0.0.0/8. Now it gets
|
||||||
|
parsed as 0.0.0.10/8.
|
||||||
|
* Make hostwatch find both fqdn and hostname.
|
||||||
|
* Use versions of python3 greater than 3.5 when available (e.g. 3.6).
|
||||||
|
|
||||||
|
Removed
|
||||||
|
~~~~~~~
|
||||||
|
* Remove Python 2.6 from automatic tests.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
~~~~~
|
||||||
|
* Fix case where there is no --dns.
|
||||||
|
* [pf] Avoid port forwarding from loopback address.
|
||||||
|
* Use getaddrinfo to obtain a correct sockaddr.
|
||||||
|
* Skip empty lines on incoming routes data.
|
||||||
|
* Just skip empty lines of routes data instead of stopping processing.
|
||||||
|
* [pf] Load pf kernel module when enabling pf.
|
||||||
|
* [pf] Test double restore (ipv4, ipv6) disables only once; test kldload.
|
||||||
|
* Fixes UDP and DNS proxies binding to the same socket address.
|
||||||
|
* Mock socket bind to avoid depending on local IPs being available in test box.
|
||||||
|
* Fix no value passed for argument auto_hosts in hw_main call.
|
||||||
|
* Fixed incorrect license information in setup.py.
|
||||||
|
* Preserve peer and port properly.
|
||||||
|
* Make --to-dns and --ns-host work well together.
|
||||||
|
* Remove test that fails under OSX.
|
||||||
|
* Specify pip requirements for tests.
|
||||||
|
* Use flake8 to find Python syntax errors or undefined names.
|
||||||
|
* Fix compatibility with the sudoers file.
|
||||||
|
* Stop using SO_REUSEADDR on sockets.
|
||||||
|
* Declare 'verbosity' as global variable to placate linters.
|
||||||
|
* Adds 'cd sshuttle' after 'git' to README and docs.
|
||||||
|
* Documentation for loading options from configuration file.
|
||||||
|
* Load options from a file.
|
||||||
|
* Fix firewall.py.
|
||||||
|
* Move sdnotify after setting up firewall rules.
|
||||||
|
* Fix tests on Macos.
|
||||||
|
|
||||||
|
|
||||||
|
0.78.3 - 2017-07-09
|
||||||
|
-------------------
|
||||||
|
The "I should have done a git pull" first release.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
~~~~~
|
||||||
|
* Order first by port range and only then by swidth
|
||||||
|
|
||||||
|
|
||||||
|
0.78.2 - 2017-07-09
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Added
|
||||||
|
~~~~~
|
||||||
|
* Adds support for tunneling specific port ranges (#144).
|
||||||
|
* Add support for iproute2.
|
||||||
|
* Allow remote hosts with colons in the username.
|
||||||
|
* Re-introduce ipfw support for sshuttle on FreeBSD with support for --DNS option as well.
|
||||||
|
* Add support for PfSense.
|
||||||
|
* Tests and documentation for systemd integration.
|
||||||
|
* Allow subnets to be given only by file (-s).
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
~~~~~
|
||||||
|
* Work around non tabular headers in BSD netstat.
|
||||||
|
* Fix UDP and DNS support on Python 2.7 with tproxy method.
|
||||||
|
* Fixed tests after adding support for iproute2.
|
||||||
|
* Small refactoring of netstat/iproute parsing.
|
||||||
|
* Set started_by_sshuttle False after disabling pf.
|
||||||
|
* Fix punctuation and explain Type=notify.
|
||||||
|
* Move pytest-runner to tests_require.
|
||||||
|
* Fix warning: closed channel got=STOP_SENDING.
|
||||||
|
* Support sdnotify for better systemd integration.
|
||||||
|
* Fix #117 to allow for no subnets via file (-s).
|
||||||
|
* Fix argument splitting for multi-word arguments.
|
||||||
|
* requirements.rst: Fix mistakes.
|
||||||
|
* Fix typo, space not required here.
|
||||||
|
* Update installation instructions.
|
||||||
|
* Support using run from different directory.
|
||||||
|
* Ensure we update sshuttle/version.py in run.
|
||||||
|
* Don't print python version in run.
|
||||||
|
* Add CWD to PYTHONPATH in run.
|
||||||
|
|
||||||
|
|
||||||
|
0.78.1 - 2016-08-06
|
||||||
|
-------------------
|
||||||
|
* Fix readthedocs versioning.
|
||||||
|
* Don't crash on ENETUNREACH.
|
||||||
|
* Various bug fixes.
|
||||||
|
* Improvements to BSD and OSX support.
|
||||||
|
|
||||||
|
|
||||||
|
0.78.0 - 2016-04-08
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* Don't force IPv6 if IPv6 nameservers supplied. Fixes #74.
|
||||||
|
* Call /bin/sh as users shell may not be POSIX compliant. Fixes #77.
|
||||||
|
* Use argparse for command line processing. Fixes #75.
|
||||||
|
* Remove useless --server option.
|
||||||
|
* Support multiple -s (subnet) options. Fixes #86.
|
||||||
|
* Make server parts work with old versions of Python. Fixes #81.
|
||||||
|
|
||||||
|
|
||||||
|
0.77.2 - 2016-03-07
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed.
|
||||||
|
|
||||||
|
|
||||||
|
0.77.1 - 2016-03-07
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* Use semantic versioning. http://semver.org/
|
||||||
|
* Update GPL 2 license text.
|
||||||
|
* New release to fix PyPI.
|
||||||
|
|
||||||
|
|
||||||
|
0.77 - 2016-03-03
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
* Various bug fixes.
|
||||||
|
* Fix Documentation.
|
||||||
|
* Add fix for MacOS X issue.
|
||||||
|
* Add support for OpenBSD.
|
||||||
|
|
||||||
|
|
||||||
|
0.76 - 2016-01-17
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
* Add option to disable IPv6 support.
|
||||||
|
* Update documentation.
|
||||||
|
* Move documentation, including man page, to Sphinx.
|
||||||
|
* Use setuptools-scm for automatic versioning.
|
||||||
|
|
||||||
|
|
||||||
|
0.75 - 2016-01-12
|
||||||
|
-----------------
|
||||||
|
|
||||||
* Revert change that broke sshuttle entry point.
|
* Revert change that broke sshuttle entry point.
|
||||||
|
|
||||||
|
|
||||||
Release 0.74 (Jan 10, 2016)
|
0.74 - 2016-01-10
|
||||||
===========================
|
-----------------
|
||||||
|
|
||||||
* Add CHANGES.rst file.
|
* Add CHANGES.rst file.
|
||||||
* Numerous bug fixes.
|
* Numerous bug fixes.
|
||||||
|
20
LICENSE
20
LICENSE
@ -1,15 +1,15 @@
|
|||||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||||
Version 2, June 1991
|
Version 2, June 1991
|
||||||
|
|
||||||
Copyright (C) 1991 Free Software Foundation, Inc.
|
Copyright (C) 1991 Free Software Foundation, Inc.
|
||||||
675 Mass Ave, Cambridge, MA 02139, USA
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
[This is the first released version of the library GPL. It is
|
[This is the first released version of the library GPL. It is
|
||||||
numbered 2 because it goes with version 2 of the ordinary GPL.]
|
numbered 2 because it goes with version 2 of the ordinary GPL.]
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
The licenses for most software are designed to take away your
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
@ -99,7 +99,7 @@ works together with the library.
|
|||||||
Note that it is possible for a library to be covered by the ordinary
|
Note that it is possible for a library to be covered by the ordinary
|
||||||
General Public License rather than by this special one.
|
General Public License rather than by this special one.
|
||||||
|
|
||||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
0. This License Agreement applies to any software library which
|
0. This License Agreement applies to any software library which
|
||||||
@ -411,7 +411,7 @@ decision will be guided by the two goals of preserving the free status
|
|||||||
of all derivatives of our free software and of promoting the sharing
|
of all derivatives of our free software and of promoting the sharing
|
||||||
and reuse of software generally.
|
and reuse of software generally.
|
||||||
|
|
||||||
NO WARRANTY
|
NO WARRANTY
|
||||||
|
|
||||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
@ -434,9 +434,9 @@ FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
|||||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
DAMAGES.
|
DAMAGES.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
Appendix: How to Apply These Terms to Your New Libraries
|
How to Apply These Terms to Your New Libraries
|
||||||
|
|
||||||
If you develop a new library, and you want it to be of the greatest
|
If you develop a new library, and you want it to be of the greatest
|
||||||
possible use to the public, we recommend making it free software that
|
possible use to the public, we recommend making it free software that
|
||||||
@ -463,8 +463,8 @@ convey the exclusion of warranty; and each file should have at least the
|
|||||||
Library General Public License for more details.
|
Library 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 Library General Public
|
||||||
License along with this library; if not, write to the Free
|
License along with this library; if not, write to the Free Software
|
||||||
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
@ -2,4 +2,13 @@ include *.txt
|
|||||||
include *.rst
|
include *.rst
|
||||||
include *.py
|
include *.py
|
||||||
include MANIFEST.in
|
include MANIFEST.in
|
||||||
|
include LICENSE
|
||||||
|
include run
|
||||||
|
include tox.ini
|
||||||
|
exclude sshuttle/version.py
|
||||||
|
recursive-include docs *.bat
|
||||||
|
recursive-include docs *.py
|
||||||
|
recursive-include docs *.rst
|
||||||
|
recursive-include docs Makefile
|
||||||
recursive-include sshuttle *.py
|
recursive-include sshuttle *.py
|
||||||
|
recursive-exclude docs/_build *
|
||||||
|
280
README.rst
280
README.rst
@ -23,245 +23,81 @@ common case:
|
|||||||
|
|
||||||
- You can't use openssh's PermitTunnel feature because
|
- You can't use openssh's PermitTunnel feature because
|
||||||
it's disabled by default on openssh servers; plus it does
|
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
|
||||||
Client side Requirements
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
- sudo, or logged in as root on your client machine.
|
|
||||||
(The server doesn't need admin access.)
|
|
||||||
- Python 2.7 or Python 3.5.
|
|
||||||
|
|
||||||
+-------+--------+------------+-----------------------------------------------+
|
|
||||||
| OS | Method | Features | Requirements |
|
|
||||||
+=======+========+============+===============================================+
|
|
||||||
| Linux | NAT | * IPv4 TCP + iptables DNAT, REDIRECT, and ttl modules. |
|
|
||||||
+ +--------+------------+-----------------------------------------------+
|
|
||||||
| | TPROXY | * IPv4 TCP + Linux with TPROXY support. |
|
|
||||||
| | | * IPv4 UDP + Python 3.5 preferred (see below). |
|
|
||||||
| | | * IPv6 TCP + |
|
|
||||||
| | | * IPv6 UDP + |
|
|
||||||
+-------+--------+------------+-----------------------------------------------+
|
|
||||||
| MacOS | PF | * IPv4 TCP + You need to have the pfctl command. |
|
|
||||||
+-------+--------+------------+-----------------------------------------------+
|
|
||||||
|
|
||||||
|
|
||||||
Server side Requirements
|
|
||||||
------------------------
|
|
||||||
Python 2.7 or Python 3.5.
|
|
||||||
|
|
||||||
|
|
||||||
Additional Suggested Software
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
- You may want to use autossh, available in various package management
|
|
||||||
systems
|
|
||||||
|
|
||||||
|
|
||||||
Additional information for TPROXY
|
|
||||||
---------------------------------
|
|
||||||
TPROXY is the only method that supports full support of IPv6 and UDP.
|
|
||||||
|
|
||||||
Full UDP or DNS support with the TPROXY method requires the `recvmsg()`
|
|
||||||
syscall. This is not available in Python 2.7, however is in Python 3.5 and
|
|
||||||
later.
|
|
||||||
|
|
||||||
- For Python 2.7, you need PyXAPI, available here:
|
|
||||||
http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
|
|
||||||
|
|
||||||
There are some things you need to consider for TPROXY to work:
|
|
||||||
|
|
||||||
- The following commands need to be run first as root. This only needs to be
|
|
||||||
done once after booting up::
|
|
||||||
|
|
||||||
ip route add local default dev lo table 100
|
|
||||||
ip rule add fwmark 1 lookup 100
|
|
||||||
ip -6 route add local default dev lo table 100
|
|
||||||
ip -6 rule add fwmark 1 lookup 100
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
- The client needs to be run as root. e.g.::
|
|
||||||
|
|
||||||
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
|
||||||
|
|
||||||
- You may need to exclude the IP address of the server you are connecting to.
|
|
||||||
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 do need the `--method=tproxy` parameter, as above.
|
|
||||||
|
|
||||||
- The routes for the outgoing packets must already exist. For example, if your
|
|
||||||
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
|
|
||||||
packets will not be generated and sshuttle cannot intercept them::
|
|
||||||
|
|
||||||
telnet -6 www.google.com 80
|
|
||||||
Trying 2404:6800:4001:805::1010...
|
|
||||||
telnet: Unable to connect to remote host: Network is unreachable
|
|
||||||
|
|
||||||
Add some dummy routes to external interfaces. Make sure they get removed
|
|
||||||
however after sshuttle exits.
|
|
||||||
|
|
||||||
|
|
||||||
Obtaining sshuttle
|
Obtaining sshuttle
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Debian stretch or later::
|
||||||
|
|
||||||
|
apt-get install sshuttle
|
||||||
|
|
||||||
|
- Arch Linux::
|
||||||
|
|
||||||
|
pacman -S sshuttle
|
||||||
|
|
||||||
|
- Fedora::
|
||||||
|
|
||||||
|
dnf install sshuttle
|
||||||
|
|
||||||
|
- NixOS::
|
||||||
|
|
||||||
|
nix-env -iA nixos.sshuttle
|
||||||
|
|
||||||
- From PyPI::
|
- From PyPI::
|
||||||
|
|
||||||
pip install sshuttle
|
sudo pip install sshuttle
|
||||||
|
|
||||||
- Clone::
|
- Clone::
|
||||||
|
|
||||||
git clone https://github.com/sshuttle/sshuttle.git
|
git clone https://github.com/sshuttle/sshuttle.git
|
||||||
|
cd sshuttle
|
||||||
|
sudo ./setup.py install
|
||||||
|
|
||||||
|
- FreeBSD::
|
||||||
|
|
||||||
|
# ports
|
||||||
|
cd /usr/ports/net/py-sshuttle && make install clean
|
||||||
|
# pkg
|
||||||
|
pkg install py36-sshuttle
|
||||||
|
|
||||||
|
It is also possible to install into a virtualenv as a non-root user.
|
||||||
|
|
||||||
|
- From PyPI::
|
||||||
|
|
||||||
|
virtualenv -p python3 /tmp/sshuttle
|
||||||
|
. /tmp/sshuttle/bin/activate
|
||||||
|
pip install sshuttle
|
||||||
|
|
||||||
|
- Clone::
|
||||||
|
|
||||||
|
virtualenv -p python3 /tmp/sshuttle
|
||||||
|
. /tmp/sshuttle/bin/activate
|
||||||
|
git clone https://github.com/sshuttle/sshuttle.git
|
||||||
|
cd sshuttle
|
||||||
./setup.py install
|
./setup.py install
|
||||||
|
|
||||||
|
- Homebrew::
|
||||||
|
|
||||||
Usage
|
brew install sshuttle
|
||||||
-----
|
|
||||||
|
|
||||||
- Forward all traffic::
|
- Nix::
|
||||||
|
|
||||||
sshuttle -r username@sshserver 0.0.0.0/0 -vv
|
nix-env -iA nixpkgs.sshuttle
|
||||||
|
|
||||||
- By default sshuttle will automatically choose a method to use. Override with
|
|
||||||
the `--method=` parameter.
|
|
||||||
|
|
||||||
- There is a shortcut for 0.0.0.0/0 for those that value
|
|
||||||
their wrists::
|
|
||||||
|
|
||||||
sshuttle -r username@sshserver 0/0 -vv
|
|
||||||
|
|
||||||
- If you would also like your DNS queries to be proxied
|
|
||||||
through the DNS server of the server you are connect to::
|
|
||||||
|
|
||||||
sshuttle --dns -vvr username@sshserver 0/0
|
|
||||||
|
|
||||||
The above is probably what you want to use to prevent
|
|
||||||
local network attacks such as Firesheep and friends.
|
|
||||||
|
|
||||||
(You may be prompted for one or more passwords; first, the local password to
|
|
||||||
become root using sudo, and then the remote ssh password. Or you might have
|
|
||||||
sudo and ssh set up to not require passwords, in which case you won't be
|
|
||||||
prompted at all.)
|
|
||||||
|
|
||||||
|
|
||||||
Usage Notes
|
Documentation
|
||||||
-----------
|
-------------
|
||||||
|
The documentation for the stable version is available at:
|
||||||
|
https://sshuttle.readthedocs.org/
|
||||||
|
|
||||||
That's it! Now your local machine can access the remote network as if you
|
The documentation for the latest development version is available at:
|
||||||
were right there. And if your "client" machine is a router, everyone on
|
https://sshuttle.readthedocs.org/en/latest/
|
||||||
your local network can make connections to your remote network.
|
|
||||||
|
|
||||||
You don't need to install sshuttle on the remote server;
|
|
||||||
the remote server just needs to have python available.
|
|
||||||
sshuttle will automatically upload and run its source code
|
|
||||||
to the remote python interpreter.
|
|
||||||
|
|
||||||
This creates a transparent proxy server on your local machine for all IP
|
|
||||||
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
|
|
||||||
you want; use any number of IP addresses or subnets to change which
|
|
||||||
addresses get proxied. Using 0.0.0.0/0 proxies *everything*, which is
|
|
||||||
interesting if you don't trust the people on your local network.)
|
|
||||||
|
|
||||||
Any TCP session you initiate to one of the proxied IP addresses will be
|
|
||||||
captured by sshuttle and sent over an ssh session to the remote copy of
|
|
||||||
sshuttle, which will then regenerate the connection on that end, and funnel
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
Support
|
Running as a service
|
||||||
-------
|
--------------------
|
||||||
|
Sshuttle can also be run as a service and configured using a config management system:
|
||||||
Mailing list:
|
https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe
|
||||||
|
|
||||||
* Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
|
|
||||||
* List archives are at: http://groups.google.com/group/sshuttle
|
|
||||||
|
|
||||||
Issue tracker and pull requests at github:
|
|
||||||
|
|
||||||
* https://github.com/sshuttle/sshuttle
|
|
||||||
|
|
||||||
|
|
||||||
Theory of Operation
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind
|
|
||||||
of both, and kind of neither.
|
|
||||||
|
|
||||||
It's like a VPN, since it can forward every port on an entire network, not
|
|
||||||
just ports you specify. Conveniently, it lets you use the "real" IP
|
|
||||||
addresses of each host rather than faking port numbers on localhost.
|
|
||||||
|
|
||||||
On the other hand, the way it *works* is more like ssh port forwarding than
|
|
||||||
a VPN. Normally, a VPN forwards your data one packet at a time, and
|
|
||||||
doesn't care about individual connections; ie. it's "stateless" with respect
|
|
||||||
to the traffic. sshuttle is the opposite of stateless; it tracks every
|
|
||||||
single connection.
|
|
||||||
|
|
||||||
You could compare sshuttle to something like the old `Slirp
|
|
||||||
<http://en.wikipedia.org/wiki/Slirp>`_ program, which was a userspace TCP/IP
|
|
||||||
implementation that did something similar. But it operated on a
|
|
||||||
packet-by-packet basis on the client side, reassembling the packets on the
|
|
||||||
server side. That worked okay back in the "real live serial port" days,
|
|
||||||
because serial ports had predictable latency and buffering.
|
|
||||||
|
|
||||||
But you can't safely just forward TCP packets over a TCP session (like ssh),
|
|
||||||
because TCP's performance depends fundamentally on packet loss; it
|
|
||||||
*must* experience packet loss in order to know when to slow down! At
|
|
||||||
the same time, the outer TCP session (ssh, in this case) is a reliable
|
|
||||||
transport, which means that what you forward through the tunnel *never*
|
|
||||||
experiences packet loss. The ssh session itself experiences packet loss, of
|
|
||||||
course, but TCP fixes it up and ssh (and thus you) never know the
|
|
||||||
difference. But neither does your inner TCP session, and extremely screwy
|
|
||||||
performance ensues.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
Useless Trivia
|
|
||||||
--------------
|
|
||||||
This section written by Avery Pennarun <apenwarr@gmail.com>.
|
|
||||||
|
|
||||||
Back in 1998 (12 years ago! Yikes!), I released the first version of `Tunnel
|
|
||||||
Vision <http://alumnit.ca/wiki/?TunnelVisionReadMe>`_, a semi-intelligent VPN
|
|
||||||
client for Linux. Unfortunately, I made two big mistakes: I implemented the
|
|
||||||
key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops).
|
|
||||||
The resulting program worked okay - and people used it for years - but the
|
|
||||||
performance was always a bit funny. And nobody ever found any security flaws
|
|
||||||
in my key exchange, either, but that doesn't mean anything. :)
|
|
||||||
|
|
||||||
The same year, dcoombs and I also released Fast Forward, a proxy server
|
|
||||||
supporting transparent proxying. Among other things, we used it for
|
|
||||||
automatically splitting traffic across more than one Internet connection (a
|
|
||||||
tool we called "Double Vision").
|
|
||||||
|
|
||||||
I was still in university at the time. A couple years after that, one of my
|
|
||||||
professors was working with some graduate students on the technology that would
|
|
||||||
eventually become `Slipstream Internet Acceleration
|
|
||||||
<http://www.slipstream.com/>`_. He asked me to do a contract for him to build
|
|
||||||
an initial prototype of a transparent proxy server for mobile networks. The
|
|
||||||
idea was similar to sshuttle: if you reassemble and then disassemble the TCP
|
|
||||||
packets, you can reduce latency and improve performance vs. just forwarding
|
|
||||||
the packets over a plain VPN or mobile network. (It's unlikely that any of my
|
|
||||||
code has persisted in the Slipstream product today, but the concept is still
|
|
||||||
pretty cool. I'm still horrified that people use plain TCP on complex mobile
|
|
||||||
networks with crazily variable latency, for which it was never really
|
|
||||||
intended.)
|
|
||||||
|
|
||||||
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, 10 years
|
|
||||||
later. You're welcome.
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
0.75
|
|
9
bandit.yml
Normal file
9
bandit.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
exclude_dirs:
|
||||||
|
- tests
|
||||||
|
skips:
|
||||||
|
- B101
|
||||||
|
- B104
|
||||||
|
- B404
|
||||||
|
- B603
|
||||||
|
- B606
|
||||||
|
- B607
|
76
bin/sudoers-add
Executable file
76
bin/sudoers-add
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# William Mantly <wmantly@gmail.com>
|
||||||
|
# MIT License
|
||||||
|
# https://github.com/wmantly/sudoers-add
|
||||||
|
|
||||||
|
NEWLINE=$'\n'
|
||||||
|
CONTENT=""
|
||||||
|
ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")"
|
||||||
|
|
||||||
|
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
|
||||||
|
echo "Usage: $ME [file_path] [sudoers-file-name]"
|
||||||
|
echo "Usage: [content] | $ME sudoers-file-name"
|
||||||
|
echo "This will take a sudoers config validate it and add it to /etc/sudoers.d/{sudoers-file-name}"
|
||||||
|
echo "The config can come from a file, first usage example or piped in second example."
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" == "" ]; then
|
||||||
|
(>&2 echo "This command take at lest one argument. See $ME --help")
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$2" == "" ]; then
|
||||||
|
FILE_NAME=$1
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
FILE_NAME=$2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "This script must be run as root"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while read -r line
|
||||||
|
do
|
||||||
|
CONTENT+="${line}${NEWLINE}"
|
||||||
|
done < "${1:-/dev/stdin}"
|
||||||
|
|
||||||
|
if [ "$CONTENT" == "" ]; then
|
||||||
|
(>&2 echo "No config content specified. See $ME --help")
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FILE_NAME" == "" ]; then
|
||||||
|
(>&2 echo "No sudoers file name specified. See $ME --help")
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make a temp file to hold the sudoers config
|
||||||
|
umask 077
|
||||||
|
TEMP_FILE=$(mktemp)
|
||||||
|
echo "$CONTENT" > "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Make sure the content is valid
|
||||||
|
visudo_STDOUT=$(visudo -c -f "$TEMP_FILE" 2>&1)
|
||||||
|
visudo_code=$?
|
||||||
|
# The temp file is no longer needed
|
||||||
|
rm "$TEMP_FILE"
|
||||||
|
|
||||||
|
if [ $visudo_code -eq 0 ]; then
|
||||||
|
echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME"
|
||||||
|
chmod 0440 "/etc/sudoers.d/$FILE_NAME"
|
||||||
|
echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Invalid sudoers config!"
|
||||||
|
echo "$visudo_STDOUT"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
177
docs/Makefile
Normal file
177
docs/Makefile
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# User-friendly check for sphinx-build
|
||||||
|
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||||
|
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " xml to make Docutils-native XML files"
|
||||||
|
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sshuttle.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sshuttle.qhc"
|
||||||
|
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/sshuttle"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sshuttle"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
latexpdfja:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
|
|
||||||
|
xml:
|
||||||
|
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||||
|
|
||||||
|
pseudoxml:
|
||||||
|
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
1
docs/changes.rst
Normal file
1
docs/changes.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../CHANGES.rst
|
12
docs/chromeos.rst
Normal file
12
docs/chromeos.rst
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Google ChromeOS
|
||||||
|
===============
|
||||||
|
|
||||||
|
Currently there is no built in support for running sshuttle directly on
|
||||||
|
Google ChromeOS/Chromebooks.
|
||||||
|
|
||||||
|
What we can really do is to create a Linux VM with Crostini. In the default
|
||||||
|
stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and
|
||||||
|
it just works, as do xterms and ssvncviewer etc.
|
||||||
|
|
||||||
|
https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide
|
||||||
|
|
261
docs/conf.py
Normal file
261
docs/conf.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# sshuttle documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Sun Jan 17 12:13:47 2016.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
import sshuttle.version # NOQA
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.todo',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
# source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = 'sshuttle'
|
||||||
|
copyright = '2016, Brian May'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = sshuttle.version.version
|
||||||
|
# The short X.Y version.
|
||||||
|
version = '.'.join(release.split('.')[:2])
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
# language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
# today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
# today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all
|
||||||
|
# documents.
|
||||||
|
# default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
# add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
# add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
# show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
# modindex_common_prefix = []
|
||||||
|
|
||||||
|
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||||
|
# keep_warnings = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
html_theme = 'default'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
# html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
# html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
# html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
# html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
# html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# Add any extra paths that contain custom files (such as robots.txt or
|
||||||
|
# .htaccess) here, relative to this directory. These files are copied
|
||||||
|
# directly to the root of the documentation.
|
||||||
|
# html_extra_path = []
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
# html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
# html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
# html_sidebars = {}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
# html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
# html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
# html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
# html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
# html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
# html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
# html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
# html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
# html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'sshuttledoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
# 'preamble': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
('index', 'sshuttle.tex', 'sshuttle documentation', 'Brian May', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
# latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
# latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
# latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
# latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
# latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
# latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
('manpage', 'sshuttle', 'sshuttle documentation', ['Brian May'], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
# man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
('index', 'sshuttle', 'sshuttle documentation',
|
||||||
|
'Brian May', 'sshuttle', 'A transparent proxy-based VPN using ssh',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
# texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
# texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
# texinfo_show_urls = 'footnote'
|
||||||
|
|
||||||
|
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||||
|
# texinfo_no_detailmenu = False
|
37
docs/how-it-works.rst
Normal file
37
docs/how-it-works.rst
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
How it works
|
||||||
|
============
|
||||||
|
sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind
|
||||||
|
of both, and kind of neither.
|
||||||
|
|
||||||
|
It's like a VPN, since it can forward every port on an entire network, not
|
||||||
|
just ports you specify. Conveniently, it lets you use the "real" IP
|
||||||
|
addresses of each host rather than faking port numbers on localhost.
|
||||||
|
|
||||||
|
On the other hand, the way it *works* is more like ssh port forwarding than
|
||||||
|
a VPN. Normally, a VPN forwards your data one packet at a time, and
|
||||||
|
doesn't care about individual connections; ie. it's "stateless" with respect
|
||||||
|
to the traffic. sshuttle is the opposite of stateless; it tracks every
|
||||||
|
single connection.
|
||||||
|
|
||||||
|
You could compare sshuttle to something like the old `Slirp
|
||||||
|
<http://en.wikipedia.org/wiki/Slirp>`_ program, which was a userspace TCP/IP
|
||||||
|
implementation that did something similar. But it operated on a
|
||||||
|
packet-by-packet basis on the client side, reassembling the packets on the
|
||||||
|
server side. That worked okay back in the "real live serial port" days,
|
||||||
|
because serial ports had predictable latency and buffering.
|
||||||
|
|
||||||
|
But you can't safely just forward TCP packets over a TCP session (like ssh),
|
||||||
|
because TCP's performance depends fundamentally on packet loss; it
|
||||||
|
*must* experience packet loss in order to know when to slow down! At
|
||||||
|
the same time, the outer TCP session (ssh, in this case) is a reliable
|
||||||
|
transport, which means that what you forward through the tunnel *never*
|
||||||
|
experiences packet loss. The ssh session itself experiences packet loss, of
|
||||||
|
course, but TCP fixes it up and ssh (and thus you) never know the
|
||||||
|
difference. But neither does your inner TCP session, and extremely screwy
|
||||||
|
performance ensues.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
29
docs/index.rst
Normal file
29
docs/index.rst
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
sshuttle: where transparent proxy meets VPN meets ssh
|
||||||
|
=====================================================
|
||||||
|
|
||||||
|
:Date: |today|
|
||||||
|
:Version: |version|
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
overview
|
||||||
|
requirements
|
||||||
|
installation
|
||||||
|
usage
|
||||||
|
platform
|
||||||
|
Man Page <manpage>
|
||||||
|
how-it-works
|
||||||
|
support
|
||||||
|
trivia
|
||||||
|
changes
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
22
docs/installation.rst
Normal file
22
docs/installation.rst
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
- From PyPI::
|
||||||
|
|
||||||
|
pip install sshuttle
|
||||||
|
|
||||||
|
- Debain package manager::
|
||||||
|
sudo apt install sshuttle
|
||||||
|
|
||||||
|
- Clone::
|
||||||
|
|
||||||
|
git clone https://github.com/sshuttle/sshuttle.git
|
||||||
|
cd sshuttle
|
||||||
|
./setup.py install
|
||||||
|
|
||||||
|
|
||||||
|
Optionally after installation
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
- Add to sudoers file
|
||||||
|
sshuttle --sudoers
|
242
docs/make.bat
Normal file
242
docs/make.bat
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set BUILDDIR=_build
|
||||||
|
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||||
|
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||||
|
if NOT "%PAPER%" == "" (
|
||||||
|
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||||
|
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
if "%1" == "help" (
|
||||||
|
:help
|
||||||
|
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||||
|
echo. html to make standalone HTML files
|
||||||
|
echo. dirhtml to make HTML files named index.html in directories
|
||||||
|
echo. singlehtml to make a single large HTML file
|
||||||
|
echo. pickle to make pickle files
|
||||||
|
echo. json to make JSON files
|
||||||
|
echo. htmlhelp to make HTML files and a HTML help project
|
||||||
|
echo. qthelp to make HTML files and a qthelp project
|
||||||
|
echo. devhelp to make HTML files and a Devhelp project
|
||||||
|
echo. epub to make an epub
|
||||||
|
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||||
|
echo. text to make text files
|
||||||
|
echo. man to make manual pages
|
||||||
|
echo. texinfo to make Texinfo files
|
||||||
|
echo. gettext to make PO message catalogs
|
||||||
|
echo. changes to make an overview over all changed/added/deprecated items
|
||||||
|
echo. xml to make Docutils-native XML files
|
||||||
|
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||||
|
echo. linkcheck to check all external links for integrity
|
||||||
|
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "clean" (
|
||||||
|
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||||
|
del /q /s %BUILDDIR%\*
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
%SPHINXBUILD% 2> nul
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.http://sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "html" (
|
||||||
|
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dirhtml" (
|
||||||
|
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "singlehtml" (
|
||||||
|
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pickle" (
|
||||||
|
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the pickle files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "json" (
|
||||||
|
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the JSON files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "htmlhelp" (
|
||||||
|
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||||
|
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "qthelp" (
|
||||||
|
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||||
|
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||||
|
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sshuttle.qhcp
|
||||||
|
echo.To view the help file:
|
||||||
|
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sshuttle.ghc
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "devhelp" (
|
||||||
|
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub" (
|
||||||
|
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latex" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdf" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf
|
||||||
|
cd %BUILDDIR%/..
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latexpdfja" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
cd %BUILDDIR%/latex
|
||||||
|
make all-pdf-ja
|
||||||
|
cd %BUILDDIR%/..
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "text" (
|
||||||
|
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "man" (
|
||||||
|
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "texinfo" (
|
||||||
|
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "gettext" (
|
||||||
|
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "changes" (
|
||||||
|
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.The overview file is in %BUILDDIR%/changes.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "linkcheck" (
|
||||||
|
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Link check complete; look for any errors in the above output ^
|
||||||
|
or in %BUILDDIR%/linkcheck/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "doctest" (
|
||||||
|
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of doctests in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/doctest/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "xml" (
|
||||||
|
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pseudoxml" (
|
||||||
|
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
:end
|
400
docs/manpage.rst
Normal file
400
docs/manpage.rst
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
sshuttle
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
**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 3.5 or higher.
|
||||||
|
|
||||||
|
To work, you must have root access on the local machine,
|
||||||
|
but you can have a normal account on the server.
|
||||||
|
|
||||||
|
It's valid to run :program:`sshuttle` more than once simultaneously on
|
||||||
|
a single client machine, connecting to a different server
|
||||||
|
every time, so you can be on more than one VPN at once.
|
||||||
|
|
||||||
|
If run on a router, :program:`sshuttle` can forward traffic for your
|
||||||
|
entire subnet to the VPN.
|
||||||
|
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
.. program:: sshuttle
|
||||||
|
|
||||||
|
.. 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.
|
||||||
|
|
||||||
|
.. option:: --method <auto|nat|nft|tproxy|pf>
|
||||||
|
|
||||||
|
Which firewall method should sshuttle use? For auto, sshuttle attempts to
|
||||||
|
guess the appropriate method depending on what it can find in PATH. The
|
||||||
|
default value is auto.
|
||||||
|
|
||||||
|
.. option:: -l <[ip:]port>, --listen=<[ip:]port>
|
||||||
|
|
||||||
|
Use this ip address and port number as the transparent
|
||||||
|
proxy port. By default :program:`sshuttle` finds an available
|
||||||
|
port automatically and listens on IP 127.0.0.1
|
||||||
|
(localhost), so you don't need to override it, and
|
||||||
|
connections are only proxied from the local machine,
|
||||||
|
not from outside machines. If you want to accept
|
||||||
|
connections from other machines on your network (ie. to
|
||||||
|
run :program:`sshuttle` on a router) try enabling IP Forwarding in
|
||||||
|
your kernel, then using ``--listen 0.0.0.0:0``.
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. option:: -H, --auto-hosts
|
||||||
|
|
||||||
|
Scan for remote hostnames and update the local /etc/hosts
|
||||||
|
file with matching entries for as long as the VPN is
|
||||||
|
open. This is nicer than changing your system's DNS
|
||||||
|
(/etc/resolv.conf) settings, for several reasons. First,
|
||||||
|
hostnames are added without domain names attached, so
|
||||||
|
you can ``ssh thatserver`` without worrying if your local
|
||||||
|
domain matches the remote one. Second, if you :program:`sshuttle`
|
||||||
|
into more than one VPN at a time, it's impossible to
|
||||||
|
use more than one DNS server at once anyway, but
|
||||||
|
:program:`sshuttle` correctly merges /etc/hosts entries between
|
||||||
|
all running copies. Third, if you're only routing a
|
||||||
|
few subnets over the VPN, you probably would prefer to
|
||||||
|
keep using your local DNS server for everything else.
|
||||||
|
|
||||||
|
.. option:: -N, --auto-nets
|
||||||
|
|
||||||
|
In addition to the subnets provided on the command
|
||||||
|
line, ask the server which subnets it thinks we should
|
||||||
|
route, and route those automatically. The suggestions
|
||||||
|
are taken automatically from the server's routing
|
||||||
|
table.
|
||||||
|
|
||||||
|
.. option:: --dns
|
||||||
|
|
||||||
|
Capture local DNS requests and forward to the remote DNS
|
||||||
|
server. All queries to any of the local system's DNS
|
||||||
|
servers (/etc/resolv.conf) will be intercepted and
|
||||||
|
resolved on the remote side of the tunnel instead, there
|
||||||
|
using the DNS specified via the :option:`--to-ns` option,
|
||||||
|
if specified.
|
||||||
|
|
||||||
|
.. option:: --ns-hosts=<server1[,server2[,server3[...]]]>
|
||||||
|
|
||||||
|
Capture local DNS requests to the specified server(s)
|
||||||
|
and forward to the remote DNS server. Contrary to the
|
||||||
|
:option:`--dns` option, this flag allows to specify the
|
||||||
|
DNS server(s) the queries to which to intercept,
|
||||||
|
instead of intercepting all DNS traffic on the local
|
||||||
|
machine. This can be useful when only certain DNS
|
||||||
|
requests should be resolved on the remote side of the
|
||||||
|
tunnel, e.g. in combination with dnsmasq.
|
||||||
|
|
||||||
|
.. option:: --to-ns=<server>
|
||||||
|
|
||||||
|
The DNS to forward requests to when remote DNS
|
||||||
|
resolution is enabled. If not given, sshuttle will
|
||||||
|
simply resolve using the system configured resolver on
|
||||||
|
the remote side (via /etc/resolv.conf on the remote
|
||||||
|
side).
|
||||||
|
|
||||||
|
.. option:: --python
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
|
||||||
|
|
||||||
|
The remote hostname and optional username and ssh
|
||||||
|
port number to use for connecting to the remote server.
|
||||||
|
For example, example.com, testuser@example.com,
|
||||||
|
testuser@example.com:2222, or example.com:2244.
|
||||||
|
|
||||||
|
.. option:: -x <subnet>, --exclude=<subnet>
|
||||||
|
|
||||||
|
Explicitly exclude this subnet from forwarding. The
|
||||||
|
format of this option is the same as the ``<subnets>``
|
||||||
|
option. To exclude more than one subnet, specify the
|
||||||
|
``-x`` option more than once. You can say something like
|
||||||
|
``0/0 -x 1.2.3.0/24`` to forward everything except the
|
||||||
|
local subnet over the VPN, for example.
|
||||||
|
|
||||||
|
.. 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.
|
||||||
|
|
||||||
|
.. option:: -v, --verbose
|
||||||
|
|
||||||
|
Print more information about the session. This option
|
||||||
|
can be used more than once for increased verbosity. By
|
||||||
|
default, :program:`sshuttle` prints only error messages.
|
||||||
|
|
||||||
|
.. option:: -e, --ssh-cmd
|
||||||
|
|
||||||
|
The command to use to connect to the remote server. The
|
||||||
|
default is just ``ssh``. Use this if your ssh client is
|
||||||
|
in a non-standard location or you want to provide extra
|
||||||
|
options to the ssh command, for example, ``-e 'ssh -v'``.
|
||||||
|
|
||||||
|
.. option:: --seed-hosts
|
||||||
|
|
||||||
|
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
|
||||||
|
for lists of local hostnames, but can speed things up
|
||||||
|
if you use this option to give it a few names to start
|
||||||
|
from.
|
||||||
|
|
||||||
|
If this option is used *without* :option:`--auto-hosts`,
|
||||||
|
then the listed hostnames will be scanned and added, but
|
||||||
|
no further hostnames will be added.
|
||||||
|
|
||||||
|
.. option:: --no-latency-control
|
||||||
|
|
||||||
|
Sacrifice latency to improve bandwidth benchmarks. ssh
|
||||||
|
uses really big socket buffers, which can overload the
|
||||||
|
connection if you start doing large file transfers,
|
||||||
|
thus making all your other sessions inside the same
|
||||||
|
tunnel go slowly. Normally, :program:`sshuttle` tries to avoid
|
||||||
|
this problem using a "fullness check" that allows only
|
||||||
|
a certain amount of outstanding data to be buffered at
|
||||||
|
a time. But on high-bandwidth links, this can leave a
|
||||||
|
lot of your bandwidth underutilized. It also makes
|
||||||
|
:program:`sshuttle` seem slow in bandwidth benchmarks (benchmarks
|
||||||
|
rarely test ping latency, which is what :program:`sshuttle` is
|
||||||
|
trying to control). This option disables the latency
|
||||||
|
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:: --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>
|
||||||
|
|
||||||
|
when using :option:`--daemon`, save :program:`sshuttle`'s pid to
|
||||||
|
*pidfilename*. The default is ``sshuttle.pid`` in the
|
||||||
|
current directory.
|
||||||
|
|
||||||
|
.. option:: --disable-ipv6
|
||||||
|
|
||||||
|
If using tproxy or pf methods, this will disable IPv6 support.
|
||||||
|
|
||||||
|
.. option:: --firewall
|
||||||
|
|
||||||
|
(internal use only) run the firewall manager. This is
|
||||||
|
the only part of :program:`sshuttle` that must run as root. If
|
||||||
|
you start :program:`sshuttle` as a non-root user, it will
|
||||||
|
automatically run ``sudo`` or ``su`` to start the firewall
|
||||||
|
manager, but the core of :program:`sshuttle` still runs as a
|
||||||
|
normal user.
|
||||||
|
|
||||||
|
.. option:: --hostwatch
|
||||||
|
|
||||||
|
(internal use only) run the hostwatch daemon. This
|
||||||
|
process runs on the server side and collects hostnames for
|
||||||
|
the :option:`--auto-hosts` option. Using this option by itself
|
||||||
|
makes it a lot easier to debug and test the :option:`--auto-hosts`
|
||||||
|
feature.
|
||||||
|
|
||||||
|
.. option:: --sudoers
|
||||||
|
|
||||||
|
sshuttle will auto generate the proper sudoers.d config file and add it.
|
||||||
|
Once this is completed, sshuttle will exit and tell the user if
|
||||||
|
it succeed or not. Do not call this options with sudo, it may generate a
|
||||||
|
incorrect config file.
|
||||||
|
|
||||||
|
.. option:: --sudoers-no-modify
|
||||||
|
|
||||||
|
sshuttle will auto generate the proper sudoers.d config and print it to
|
||||||
|
stdout. The option will not modify the system at all.
|
||||||
|
|
||||||
|
.. option:: --sudoers-user
|
||||||
|
|
||||||
|
Set the user name or group with %group_name for passwordless operation.
|
||||||
|
Default is the current user.set ALL for all users. Only works with
|
||||||
|
--sudoers or --sudoers-no-modify option.
|
||||||
|
|
||||||
|
.. option:: --sudoers-filename
|
||||||
|
|
||||||
|
Set the file name for the sudoers.d file to be added. Default is
|
||||||
|
"sshuttle_auto". Only works with --sudoers.
|
||||||
|
|
||||||
|
.. option:: --version
|
||||||
|
|
||||||
|
Print program version.
|
||||||
|
|
||||||
|
|
||||||
|
Configuration File
|
||||||
|
------------------
|
||||||
|
All the options described above can optionally be specified in a configuration
|
||||||
|
file.
|
||||||
|
|
||||||
|
To run :program:`sshuttle` with options defined in, e.g., `/etc/sshuttle.conf`
|
||||||
|
just pass the path to the file preceded by the `@` character, e.g.
|
||||||
|
`@/etc/sshuttle.conf`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Arguments read from a file must be one per line, as shown below::
|
||||||
|
|
||||||
|
value
|
||||||
|
--option1
|
||||||
|
value1
|
||||||
|
--option2
|
||||||
|
value2
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
Test locally by proxying all local connections, without using ssh::
|
||||||
|
|
||||||
|
$ sshuttle -v 0/0
|
||||||
|
|
||||||
|
Starting sshuttle proxy.
|
||||||
|
Listening on ('0.0.0.0', 12300).
|
||||||
|
[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...
|
||||||
|
^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
|
||||||
|
|
||||||
|
Test connection 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...
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file::
|
||||||
|
|
||||||
|
$ sshuttle @/etc/sshuttle.conf
|
||||||
|
|
||||||
|
Use the options defined in `/etc/sshuttle.conf` but be more verbose::
|
||||||
|
|
||||||
|
$ sshuttle @/etc/sshuttle.conf -vvv
|
||||||
|
|
||||||
|
Override the remote server defined in `/etc/sshuttle.conf`::
|
||||||
|
|
||||||
|
$ sshuttle @/etc/sshuttle.conf -r otheruser@test.example.com
|
||||||
|
|
||||||
|
Example configuration file::
|
||||||
|
|
||||||
|
192.168.0.0/16
|
||||||
|
--remote
|
||||||
|
user@example.com
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
After connecting to the remote server, :program:`sshuttle` uploads its
|
||||||
|
(python) source code to the remote end and executes it
|
||||||
|
there. Thus, you don't need to install :program:`sshuttle` on the
|
||||||
|
remote server, and there are never :program:`sshuttle` version
|
||||||
|
conflicts between client and server.
|
||||||
|
|
||||||
|
Unlike most VPNs, :program:`sshuttle` forwards sessions, not packets.
|
||||||
|
That is, it uses kernel transparent proxying (`iptables
|
||||||
|
REDIRECT` rules on Linux) to
|
||||||
|
capture outgoing TCP sessions, then creates entirely
|
||||||
|
separate TCP sessions out to the original destination at
|
||||||
|
the other end of the tunnel.
|
||||||
|
|
||||||
|
Packet-level forwarding (eg. using the tun/tap devices on
|
||||||
|
Linux) seems elegant at first, but it results in
|
||||||
|
several problems, notably the 'tcp over tcp' problem. The
|
||||||
|
tcp protocol depends fundamentally on packets being dropped
|
||||||
|
in order to implement its congestion control agorithm; if
|
||||||
|
you pass tcp packets through a tcp-based tunnel (such as
|
||||||
|
ssh), the inner tcp packets will never be dropped, and so
|
||||||
|
the inner tcp stream's congestion control will be
|
||||||
|
completely broken, and performance will be terrible. Thus,
|
||||||
|
packet-based VPNs (such as IPsec and openvpn) cannot use
|
||||||
|
tcp-based encrypted streams like ssh or ssl, and have to
|
||||||
|
implement their own encryption from scratch, which is very
|
||||||
|
complex and error prone.
|
||||||
|
|
||||||
|
:program:`sshuttle`'s simplicity comes from the fact that it can
|
||||||
|
safely use the existing ssh encrypted tunnel without
|
||||||
|
incurring a performance penalty. It does this by letting
|
||||||
|
the client-side kernel manage the incoming tcp stream, and
|
||||||
|
the server-side kernel manage the outgoing tcp stream;
|
||||||
|
there is no need for congestion control to be shared
|
||||||
|
between the two separate streams, so a tcp-based tunnel is
|
||||||
|
fine.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:manpage:`ssh(1)`, :manpage:`python(1)`
|
8
docs/openwrt.rst
Normal file
8
docs/openwrt.rst
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
OpenWRT
|
||||||
|
========
|
||||||
|
|
||||||
|
Run::
|
||||||
|
|
||||||
|
opkg install python3 python3-pip iptables-mod-nat-extra iptables-mod-ipopt
|
||||||
|
python3 /usr/bin/pip3 install sshuttle
|
||||||
|
sshuttle -l 0.0.0.0 -r <IP> -x 192.168.1.1 0/0
|
26
docs/overview.rst
Normal file
26
docs/overview.rst
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
As far as I know, sshuttle is the only program that solves the following
|
||||||
|
common case:
|
||||||
|
|
||||||
|
- Your client machine (or router) is Linux, MacOS, FreeBSD, OpenBSD or pfSense.
|
||||||
|
|
||||||
|
- You have access to a remote network via ssh.
|
||||||
|
|
||||||
|
- You don't necessarily have admin access on the remote network.
|
||||||
|
|
||||||
|
- The remote network has no VPN, or only stupid/complex VPN
|
||||||
|
protocols (IPsec, PPTP, etc). Or maybe you *are* the
|
||||||
|
admin and you just got frustrated with the awful state of
|
||||||
|
VPN tools.
|
||||||
|
|
||||||
|
- You don't want to create an ssh port forward for every
|
||||||
|
single host/port on the remote network.
|
||||||
|
|
||||||
|
- You hate openssh's port forwarding because it's randomly
|
||||||
|
slow and/or stupid.
|
||||||
|
|
||||||
|
- 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).
|
12
docs/platform.rst
Normal file
12
docs/platform.rst
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Platform Specific Notes
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
chromeos
|
||||||
|
tproxy
|
||||||
|
windows
|
||||||
|
openwrt
|
84
docs/requirements.rst
Normal file
84
docs/requirements.rst
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
Requirements
|
||||||
|
============
|
||||||
|
|
||||||
|
Client side Requirements
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- sudo, or root access on your client machine.
|
||||||
|
(The server doesn't need admin access.)
|
||||||
|
- Python 3.5 or greater.
|
||||||
|
|
||||||
|
|
||||||
|
Linux with NAT method
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Supports:
|
||||||
|
|
||||||
|
* IPv4 TCP
|
||||||
|
* IPv4 DNS
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* iptables DNAT, REDIRECT, and ttl modules.
|
||||||
|
|
||||||
|
|
||||||
|
Linux with TPROXY method
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Supports:
|
||||||
|
|
||||||
|
* IPv4 TCP
|
||||||
|
* IPv4 UDP (requires ``recvmsg`` - see below)
|
||||||
|
* IPv6 DNS (requires ``recvmsg`` - see below)
|
||||||
|
* IPv6 TCP
|
||||||
|
* IPv6 UDP (requires ``recvmsg`` - see below)
|
||||||
|
* IPv6 DNS (requires ``recvmsg`` - see below)
|
||||||
|
|
||||||
|
|
||||||
|
MacOS / FreeBSD / OpenBSD / pfSense
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Method: pf
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
|
||||||
|
* IPv4 TCP
|
||||||
|
* IPv4 DNS
|
||||||
|
* IPv6 TCP
|
||||||
|
* IPv6 DNS
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* You need to have the pfctl command.
|
||||||
|
|
||||||
|
Windows
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
Not officially supported, however can be made to work with Vagrant. Requires
|
||||||
|
cmd.exe with Administrator access. See :doc:`windows` for more information.
|
||||||
|
|
||||||
|
|
||||||
|
Server side Requirements
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- Python 3.5 or greater.
|
||||||
|
|
||||||
|
|
||||||
|
Additional Suggested Software
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
- 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
|
||||||
|
sshuttle service unit to notify, as shown in the example below.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
:emphasize-lines: 6
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=sshuttle
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
ExecStart=/usr/bin/sshuttle --dns --remote <user>@<server> <subnets...>
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
11
docs/support.rst
Normal file
11
docs/support.rst
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Support
|
||||||
|
=======
|
||||||
|
|
||||||
|
Mailing list:
|
||||||
|
|
||||||
|
* Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
|
||||||
|
* List archives are at: http://groups.google.com/group/sshuttle
|
||||||
|
|
||||||
|
Issue tracker and pull requests at github:
|
||||||
|
|
||||||
|
* https://github.com/sshuttle/sshuttle
|
42
docs/tproxy.rst
Normal file
42
docs/tproxy.rst
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
TPROXY
|
||||||
|
======
|
||||||
|
TPROXY is the only method that has full support of IPv6 and UDP.
|
||||||
|
|
||||||
|
There are some things you need to consider for TPROXY to work:
|
||||||
|
|
||||||
|
- The following commands need to be run first as root. This only needs to be
|
||||||
|
done once after booting up::
|
||||||
|
|
||||||
|
ip route add local default dev lo table 100
|
||||||
|
ip rule add fwmark 1 lookup 100
|
||||||
|
ip -6 route add local default dev lo table 100
|
||||||
|
ip -6 rule add fwmark 1 lookup 100
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- The client needs to be run as root. e.g.::
|
||||||
|
|
||||||
|
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
||||||
|
|
||||||
|
- You may need to exclude the IP address of the server you are connecting to.
|
||||||
|
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
|
||||||
|
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
|
||||||
|
packets will not be generated and sshuttle cannot intercept them::
|
||||||
|
|
||||||
|
telnet -6 www.google.com 80
|
||||||
|
Trying 2404:6800:4001:805::1010...
|
||||||
|
telnet: Unable to connect to remote host: Network is unreachable
|
||||||
|
|
||||||
|
Add some dummy routes to external interfaces. Make sure they get removed
|
||||||
|
however after sshuttle exits.
|
36
docs/trivia.rst
Normal file
36
docs/trivia.rst
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
Useless Trivia
|
||||||
|
==============
|
||||||
|
This section written by the original author, Avery Pennarun
|
||||||
|
<apenwarr@gmail.com>.
|
||||||
|
|
||||||
|
Back in 1998, I released the first version of `Tunnel
|
||||||
|
Vision <http://alumnit.ca/wiki/?TunnelVisionReadMe>`_, a semi-intelligent VPN
|
||||||
|
client for Linux. Unfortunately, I made two big mistakes: I implemented the
|
||||||
|
key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops).
|
||||||
|
The resulting program worked okay - and people used it for years - but the
|
||||||
|
performance was always a bit funny. And nobody ever found any security flaws
|
||||||
|
in my key exchange, either, but that doesn't mean anything. :)
|
||||||
|
|
||||||
|
The same year, dcoombs and I also released Fast Forward, a proxy server
|
||||||
|
supporting transparent proxying. Among other things, we used it for
|
||||||
|
automatically splitting traffic across more than one Internet connection (a
|
||||||
|
tool we called "Double Vision").
|
||||||
|
|
||||||
|
I was still in university at the time. A couple years after that, one of my
|
||||||
|
professors was working with some graduate students on the technology that would
|
||||||
|
eventually become `Slipstream Internet Acceleration
|
||||||
|
<http://www.slipstream.com/>`_. He asked me to do a contract for him to build
|
||||||
|
an initial prototype of a transparent proxy server for mobile networks. The
|
||||||
|
idea was similar to sshuttle: if you reassemble and then disassemble the TCP
|
||||||
|
packets, you can reduce latency and improve performance vs. just forwarding
|
||||||
|
the packets over a plain VPN or mobile network. (It's unlikely that any of my
|
||||||
|
code has persisted in the Slipstream product today, but the concept is still
|
||||||
|
pretty cool. I'm still horrified that people use plain TCP on complex mobile
|
||||||
|
networks with crazily variable latency, for which it was never really
|
||||||
|
intended.)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
110
docs/usage.rst
Normal file
110
docs/usage.rst
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
For information on usage with Windows, see the :doc:`windows` section.
|
||||||
|
For information on using the TProxy method, see the :doc:`tproxy` section.
|
||||||
|
|
||||||
|
Forward all traffic::
|
||||||
|
|
||||||
|
sshuttle -r username@sshserver 0.0.0.0/0
|
||||||
|
|
||||||
|
- Use the :option:`sshuttle -r` parameter to specify a remote server.
|
||||||
|
|
||||||
|
- By default sshuttle will automatically choose a method to use. Override with
|
||||||
|
the :option:`sshuttle --method` parameter.
|
||||||
|
|
||||||
|
- There is a shortcut for 0.0.0.0/0 for those that value
|
||||||
|
their wrists::
|
||||||
|
|
||||||
|
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::
|
||||||
|
|
||||||
|
sshuttle --dns -r username@sshserver 0/0
|
||||||
|
|
||||||
|
The above is probably what you want to use to prevent
|
||||||
|
local network attacks such as Firesheep and friends.
|
||||||
|
See the documentation for the :option:`sshuttle --dns` parameter.
|
||||||
|
|
||||||
|
(You may be prompted for one or more passwords; first, the local password to
|
||||||
|
become root using sudo, and then the remote ssh password. Or you might have
|
||||||
|
sudo and ssh set up to not require passwords, in which case you won't be
|
||||||
|
prompted at all.)
|
||||||
|
|
||||||
|
|
||||||
|
Usage Notes
|
||||||
|
-----------
|
||||||
|
That's it! Now your local machine can access the remote network as if you
|
||||||
|
were right there. And if your "client" machine is a router, everyone on
|
||||||
|
your local network can make connections to your remote network.
|
||||||
|
|
||||||
|
You don't need to install sshuttle on the remote server;
|
||||||
|
the remote server just needs to have python available.
|
||||||
|
sshuttle will automatically upload and run its source code
|
||||||
|
to the remote python interpreter.
|
||||||
|
|
||||||
|
This creates a transparent proxy server on your local machine for all IP
|
||||||
|
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
|
||||||
|
you want; use any number of IP addresses or subnets to change which
|
||||||
|
addresses get proxied. Using 0.0.0.0/0 proxies *everything*, which is
|
||||||
|
interesting if you don't trust the people on your local network.)
|
||||||
|
|
||||||
|
Any TCP session you initiate to one of the proxied IP addresses will be
|
||||||
|
captured by sshuttle and sent over an ssh session to the remote copy of
|
||||||
|
sshuttle, which will then regenerate the connection on that end, and funnel
|
||||||
|
the data back and forth through ssh.
|
||||||
|
|
||||||
|
Fun, right? A poor man's instant VPN, and you don't even have to have
|
||||||
|
admin access on the server.
|
||||||
|
|
||||||
|
Sudoers File
|
||||||
|
------------
|
||||||
|
sshuttle can auto-generate the proper sudoers.d file using the current user
|
||||||
|
for Linux and OSX. Doing this will allow sshuttle to run without asking for
|
||||||
|
the local sudo password and to give users who do not have sudo access
|
||||||
|
ability to run sshuttle::
|
||||||
|
|
||||||
|
sshuttle --sudoers
|
||||||
|
|
||||||
|
DO NOT run this command with sudo, it will ask for your sudo password when
|
||||||
|
it is needed.
|
||||||
|
|
||||||
|
A costume user or group can be set with the :
|
||||||
|
option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid
|
||||||
|
values for this vary based on how your system is configured. Values such as
|
||||||
|
usernames, groups pre-pended with `%` and sudoers user aliases will work. See
|
||||||
|
the sudoers manual for more information on valid user specif actions.
|
||||||
|
The options must be used with `--sudoers`::
|
||||||
|
|
||||||
|
sshuttle --sudoers --sudoers-user mike
|
||||||
|
sshuttle --sudoers --sudoers-user %sudo
|
||||||
|
|
||||||
|
The name of the file to be added to sudoers.d can be configured as well. This
|
||||||
|
is mostly not necessary but can be useful for giving more than one user
|
||||||
|
access to sshuttle. The default is `sshuttle_auto`::
|
||||||
|
|
||||||
|
sshuttle --sudoer --sudoers-filename sshuttle_auto_mike
|
||||||
|
sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy
|
||||||
|
|
||||||
|
You can also see what configuration will be added to your system without
|
||||||
|
modifying anything. This can be helpfull is the auto feature does not work, or
|
||||||
|
you want more control. This option also works with `--sudoers-username`.
|
||||||
|
`--sudoers-filename` has no effect with this option::
|
||||||
|
|
||||||
|
sshuttle --sudoers-no-modify
|
||||||
|
|
||||||
|
This will simply sprint the generated configuration to STDOUT. Example::
|
||||||
|
|
||||||
|
08:40 PM william$ sshuttle --sudoers-no-modify
|
||||||
|
|
||||||
|
Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall
|
||||||
|
|
||||||
|
william ALL=NOPASSWD: SSHUTTLE304
|
19
docs/windows.rst
Normal file
19
docs/windows.rst
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Microsoft Windows
|
||||||
|
=================
|
||||||
|
Currently there is no built in support for running sshuttle directly on
|
||||||
|
Microsoft Windows.
|
||||||
|
|
||||||
|
What we can really do is to create a Linux VM with Vagrant (or simply
|
||||||
|
Virtualbox if you like). In the Vagrant settings, remember to turn on bridged
|
||||||
|
NIC. Then, run sshuttle inside the VM like below::
|
||||||
|
|
||||||
|
sshuttle -l 0.0.0.0 -x 10.0.0.0/8 -x 192.168.0.0/16 0/0
|
||||||
|
|
||||||
|
10.0.0.0/8 excludes NAT traffic of Vagrant and 192.168.0.0/16 excludes
|
||||||
|
traffic to local area network (assuming that we're using 192.168.0.0 subnet).
|
||||||
|
|
||||||
|
Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can
|
||||||
|
configure that in Vagrant), we can then ask Windows to route all its traffic
|
||||||
|
via the VM by running the following in cmd.exe with admin right::
|
||||||
|
|
||||||
|
route add 0.0.0.0 mask 0.0.0.0 192.168.1.200
|
7
requirements-tests.txt
Normal file
7
requirements-tests.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
attrs==19.3.0
|
||||||
|
pytest==5.4.3
|
||||||
|
pytest-cov==2.10.0
|
||||||
|
mock==2.0.0
|
||||||
|
flake8==3.8.3
|
||||||
|
pyflakes==2.2.0
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
setuptools-scm==4.1.2
|
21
run
21
run
@ -1,6 +1,15 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env sh
|
||||||
if python3.5 -V 2>/dev/null; then
|
set -e
|
||||||
exec python3 -m "sshuttle" "$@"
|
export PYTHONPATH="$(dirname $0):$PYTHONPATH"
|
||||||
else
|
export PATH="$(dirname $0)/bin:$PATH"
|
||||||
exec python -m "sshuttle" "$@"
|
|
||||||
fi
|
python_best_version() {
|
||||||
|
if [ -x "$(command -v python3)" ] &&
|
||||||
|
python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
|
||||||
|
exec python3 "$@"
|
||||||
|
else
|
||||||
|
exec python "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
python_best_version -m "sshuttle" "$@"
|
||||||
|
17
setup.cfg
Normal file
17
setup.cfg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[aliases]
|
||||||
|
test=pytest
|
||||||
|
|
||||||
|
[bdist_wheel]
|
||||||
|
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
|
54
setup.py
54
setup.py
@ -2,52 +2,72 @@
|
|||||||
|
|
||||||
# Copyright 2012-2014 Brian May
|
# Copyright 2012-2014 Brian May
|
||||||
#
|
#
|
||||||
# This file is part of python-tldap.
|
# This file is part of sshuttle.
|
||||||
#
|
#
|
||||||
# python-tldap is free software: you can redistribute it and/or modify
|
# sshuttle is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU Lesser General Public License as
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
# published by the Free Software Foundation; either version 2.1 of
|
||||||
# (at your option) any later version.
|
# the License, or (at your option) any later version.
|
||||||
#
|
#
|
||||||
# python-tldap is distributed in the hope that it will be useful,
|
# sshuttle is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU Lesser General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with python-tldap If not, see <http://www.gnu.org/licenses/>.
|
# along with sshuttle; If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
with open('VERSION.txt', 'r') as f:
|
|
||||||
version = f.readline().strip()
|
def version_scheme(version):
|
||||||
|
from setuptools_scm.version import guess_next_dev_version
|
||||||
|
version = guess_next_dev_version(version)
|
||||||
|
return version.lstrip("v")
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="sshuttle",
|
name="sshuttle",
|
||||||
version=version,
|
use_scm_version={
|
||||||
|
'write_to': "sshuttle/version.py",
|
||||||
|
'version_scheme': version_scheme,
|
||||||
|
},
|
||||||
|
setup_requires=['setuptools_scm'],
|
||||||
|
# version=version,
|
||||||
url='https://github.com/sshuttle/sshuttle',
|
url='https://github.com/sshuttle/sshuttle',
|
||||||
author='Brian May',
|
author='Brian May',
|
||||||
author_email='brian@linuxpenguins.xyz',
|
author_email='brian@linuxpenguins.xyz',
|
||||||
description='Full-featured" VPN over an SSH tunnel',
|
description='Full-featured" VPN over an SSH tunnel',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
license="GPL2+",
|
license="LGPL2.1+",
|
||||||
long_description=open('README.rst').read(),
|
long_description=open('README.rst').read(),
|
||||||
|
long_description_content_type="text/x-rst",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Intended Audience :: End Users/Desktop",
|
"Intended Audience :: End Users/Desktop",
|
||||||
"License :: OSI Approved :: "
|
"License :: OSI Approved :: "
|
||||||
"GNU General Public License v2 or later (GPLv2+)",
|
"GNU Lesser General Public License v2 or later (LGPLv2+)",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 2.7",
|
|
||||||
"Programming Language :: Python :: 3.5",
|
"Programming Language :: Python :: 3.5",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
"Topic :: System :: Networking",
|
"Topic :: System :: Networking",
|
||||||
],
|
],
|
||||||
|
scripts=['bin/sudoers-add'],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'sshuttle = sshuttle.__main__',
|
'sshuttle = sshuttle.cmdline:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
tests_require=['pytest', 'mock'],
|
python_requires='>=3.5',
|
||||||
|
tests_require=[
|
||||||
|
'pytest',
|
||||||
|
'pytest-cov',
|
||||||
|
'pytest-runner',
|
||||||
|
'mock',
|
||||||
|
'flake8',
|
||||||
|
],
|
||||||
keywords="ssh vpn",
|
keywords="ssh vpn",
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
try:
|
||||||
|
from sshuttle.version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = "unknown"
|
||||||
|
@ -1,230 +1,4 @@
|
|||||||
|
"""Coverage.py's main entry point."""
|
||||||
import sys
|
import sys
|
||||||
import re
|
from sshuttle.cmdline import main
|
||||||
import socket
|
sys.exit(main())
|
||||||
import sshuttle.helpers as helpers
|
|
||||||
import sshuttle.options as options
|
|
||||||
import sshuttle.client as client
|
|
||||||
import sshuttle.firewall as firewall
|
|
||||||
import sshuttle.hostwatch as hostwatch
|
|
||||||
import sshuttle.ssyslog as ssyslog
|
|
||||||
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
|
||||||
|
|
||||||
|
|
||||||
# 1.2.3.4/5 or just 1.2.3.4
|
|
||||||
def parse_subnet4(s):
|
|
||||||
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
|
||||||
(a, b, c, d, width) = m.groups()
|
|
||||||
(a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
|
|
||||||
if width is None:
|
|
||||||
width = 32
|
|
||||||
else:
|
|
||||||
width = int(width)
|
|
||||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
|
||||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
|
||||||
if width > 32:
|
|
||||||
raise Fatal('*/%d is greater than the maximum of 32' % width)
|
|
||||||
return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width)
|
|
||||||
|
|
||||||
|
|
||||||
# 1:2::3/64 or just 1:2::3
|
|
||||||
def parse_subnet6(s):
|
|
||||||
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%r is not a valid IP subnet format' % s)
|
|
||||||
(net, width) = m.groups()
|
|
||||||
if width is None:
|
|
||||||
width = 128
|
|
||||||
else:
|
|
||||||
width = int(width)
|
|
||||||
if width > 128:
|
|
||||||
raise Fatal('*/%d is greater than the maximum of 128' % width)
|
|
||||||
return(socket.AF_INET6, net, width)
|
|
||||||
|
|
||||||
|
|
||||||
# Subnet file, supporting empty lines and hash-started comment lines
|
|
||||||
def parse_subnet_file(s):
|
|
||||||
try:
|
|
||||||
handle = open(s, 'r')
|
|
||||||
except OSError:
|
|
||||||
raise Fatal('Unable to open subnet file: %s' % s)
|
|
||||||
|
|
||||||
raw_config_lines = handle.readlines()
|
|
||||||
config_lines = []
|
|
||||||
for line_no, line in enumerate(raw_config_lines):
|
|
||||||
line = line.strip()
|
|
||||||
if len(line) == 0:
|
|
||||||
continue
|
|
||||||
if line[0] == '#':
|
|
||||||
continue
|
|
||||||
config_lines.append(line)
|
|
||||||
|
|
||||||
return config_lines
|
|
||||||
|
|
||||||
|
|
||||||
# list of:
|
|
||||||
# 1.2.3.4/5 or just 1.2.3.4
|
|
||||||
# 1:2::3/64 or just 1:2::3
|
|
||||||
def parse_subnets(subnets_str):
|
|
||||||
subnets = []
|
|
||||||
for s in subnets_str:
|
|
||||||
if ':' in s:
|
|
||||||
subnet = parse_subnet6(s)
|
|
||||||
else:
|
|
||||||
subnet = parse_subnet4(s)
|
|
||||||
subnets.append(subnet)
|
|
||||||
return subnets
|
|
||||||
|
|
||||||
|
|
||||||
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
|
||||||
def parse_ipport4(s):
|
|
||||||
s = str(s)
|
|
||||||
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%r is not a valid IP:port format' % s)
|
|
||||||
(a, b, c, d, port) = m.groups()
|
|
||||||
(a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
|
|
||||||
int(port or 0))
|
|
||||||
if a > 255 or b > 255 or c > 255 or d > 255:
|
|
||||||
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
|
|
||||||
if port > 65535:
|
|
||||||
raise Fatal('*:%d is greater than the maximum of 65535' % port)
|
|
||||||
if a is None:
|
|
||||||
a = b = c = d = 0
|
|
||||||
return ('%d.%d.%d.%d' % (a, b, c, d), port)
|
|
||||||
|
|
||||||
|
|
||||||
# [1:2::3]:456 or [1:2::3] or 456
|
|
||||||
def parse_ipport6(s):
|
|
||||||
s = str(s)
|
|
||||||
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
|
|
||||||
if not m:
|
|
||||||
raise Fatal('%s is not a valid IP:port format' % s)
|
|
||||||
(ip, port) = m.groups()
|
|
||||||
(ip, port) = (ip or '::', int(port or 0))
|
|
||||||
return (ip, port)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_list(list):
|
|
||||||
return re.split(r'[\s,]+', list.strip()) if list else []
|
|
||||||
|
|
||||||
|
|
||||||
optspec = """
|
|
||||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
|
||||||
sshuttle --firewall <port> <subnets...>
|
|
||||||
sshuttle --hostwatch
|
|
||||||
--
|
|
||||||
l,listen= transproxy to this ip address and port number
|
|
||||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
|
||||||
N,auto-nets automatically determine subnets to route
|
|
||||||
dns capture local DNS requests and forward to the remote DNS server
|
|
||||||
ns-hosts= capture and forward remote DNS requests to the following servers
|
|
||||||
method= auto, nat, tproxy or pf
|
|
||||||
python= path to python interpreter on the remote server
|
|
||||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
|
||||||
x,exclude= exclude this subnet (can be used more than once)
|
|
||||||
X,exclude-from= exclude the subnets in a file (whitespace separated)
|
|
||||||
v,verbose increase debug message verbosity
|
|
||||||
e,ssh-cmd= the command to use to connect to the remote [ssh]
|
|
||||||
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
|
|
||||||
no-latency-control sacrifice latency to improve bandwidth benchmarks
|
|
||||||
wrap= restart counting channel numbers after this number (for testing)
|
|
||||||
D,daemon run in the background as a daemon
|
|
||||||
s,subnets= file where the subnets are stored, instead of on the command line
|
|
||||||
syslog send log messages to syslog (default if you use --daemon)
|
|
||||||
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
|
|
||||||
server (internal use only)
|
|
||||||
firewall (internal use only)
|
|
||||||
hostwatch (internal use only)
|
|
||||||
"""
|
|
||||||
o = options.Options(optspec)
|
|
||||||
(opt, flags, extra) = o.parse(sys.argv[1:])
|
|
||||||
|
|
||||||
if opt.daemon:
|
|
||||||
opt.syslog = 1
|
|
||||||
if opt.wrap:
|
|
||||||
import sshuttle.ssnet as ssnet
|
|
||||||
ssnet.MAX_CHANNEL = int(opt.wrap)
|
|
||||||
helpers.verbose = opt.verbose or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
if opt.firewall:
|
|
||||||
if len(extra) != 0:
|
|
||||||
o.fatal('exactly zero arguments expected')
|
|
||||||
result = firewall.main(opt.method, opt.syslog)
|
|
||||||
sys.exit(result)
|
|
||||||
elif opt.hostwatch:
|
|
||||||
sys.exit(hostwatch.hw_main(extra))
|
|
||||||
else:
|
|
||||||
if len(extra) < 1 and not opt.auto_nets and not opt.subnets:
|
|
||||||
o.fatal('at least one subnet, subnet file, or -N expected')
|
|
||||||
includes = extra
|
|
||||||
excludes = ['127.0.0.0/8']
|
|
||||||
for k, v in flags:
|
|
||||||
if k in ('-x', '--exclude'):
|
|
||||||
excludes.append(v)
|
|
||||||
if k in ('-X', '--exclude-from'):
|
|
||||||
excludes += open(v).read().split()
|
|
||||||
remotename = opt.remote
|
|
||||||
if remotename == '' or remotename == '-':
|
|
||||||
remotename = None
|
|
||||||
nslist = [family_ip_tuple(ns) for ns in parse_list(opt.ns_hosts)]
|
|
||||||
if opt.seed_hosts and not opt.auto_hosts:
|
|
||||||
o.fatal('--seed-hosts only works if you also use -H')
|
|
||||||
if opt.seed_hosts:
|
|
||||||
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
|
|
||||||
elif opt.auto_hosts:
|
|
||||||
sh = []
|
|
||||||
else:
|
|
||||||
sh = None
|
|
||||||
if opt.subnets:
|
|
||||||
includes = parse_subnet_file(opt.subnets)
|
|
||||||
if not opt.method:
|
|
||||||
method_name = "auto"
|
|
||||||
elif opt.method in ["auto", "nat", "tproxy", "pf"]:
|
|
||||||
method_name = opt.method
|
|
||||||
else:
|
|
||||||
o.fatal("method_name %s not supported" % opt.method)
|
|
||||||
if not opt.listen:
|
|
||||||
ipport_v6 = "auto" # parse_ipport6('[::1]:0')
|
|
||||||
ipport_v4 = "auto" # parse_ipport4('127.0.0.1:0')
|
|
||||||
else:
|
|
||||||
ipport_v6 = None
|
|
||||||
ipport_v4 = None
|
|
||||||
list = opt.listen.split(",")
|
|
||||||
for ip in list:
|
|
||||||
if '[' in ip and ']' in ip:
|
|
||||||
ipport_v6 = parse_ipport6(ip)
|
|
||||||
else:
|
|
||||||
ipport_v4 = parse_ipport4(ip)
|
|
||||||
if opt.syslog:
|
|
||||||
ssyslog.start_syslog()
|
|
||||||
ssyslog.stderr_to_syslog()
|
|
||||||
return_code = client.main(ipport_v6, ipport_v4,
|
|
||||||
opt.ssh_cmd,
|
|
||||||
remotename,
|
|
||||||
opt.python,
|
|
||||||
opt.latency_control,
|
|
||||||
opt.dns,
|
|
||||||
nslist,
|
|
||||||
method_name,
|
|
||||||
sh,
|
|
||||||
opt.auto_nets,
|
|
||||||
parse_subnets(includes),
|
|
||||||
parse_subnets(excludes),
|
|
||||||
opt.daemon, opt.pidfile)
|
|
||||||
|
|
||||||
if return_code == 0:
|
|
||||||
log('Normal exit code, exiting...')
|
|
||||||
else:
|
|
||||||
log('Abnormal exit code detected, failing...' % return_code)
|
|
||||||
sys.exit(return_code)
|
|
||||||
|
|
||||||
except Fatal as e:
|
|
||||||
log('fatal: %s\n' % e)
|
|
||||||
sys.exit(99)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
log('\n')
|
|
||||||
log('Keyboard interrupt: exiting.\n')
|
|
||||||
sys.exit(1)
|
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
import imp
|
import types
|
||||||
|
|
||||||
|
verbosity = verbosity # noqa: F821 must be a previously defined global
|
||||||
z = zlib.decompressobj()
|
z = zlib.decompressobj()
|
||||||
while 1:
|
while 1:
|
||||||
name = stdin.readline().strip()
|
name = sys.stdin.readline().strip()
|
||||||
if name:
|
if name:
|
||||||
name = name.decode("ASCII")
|
name = name.decode("ASCII")
|
||||||
|
|
||||||
nbytes = int(stdin.readline())
|
nbytes = int(sys.stdin.readline())
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
||||||
% (name, nbytes))
|
% (name, nbytes))
|
||||||
content = z.decompress(stdin.read(nbytes))
|
content = z.decompress(sys.stdin.read(nbytes))
|
||||||
|
|
||||||
module = imp.new_module(name)
|
module = types.ModuleType(name)
|
||||||
parent, _, parent_name = name.rpartition(".")
|
parents = name.rsplit(".", 1)
|
||||||
if parent != "":
|
if len(parents) == 2:
|
||||||
|
parent, parent_name = parents
|
||||||
setattr(sys.modules[parent], parent_name, module)
|
setattr(sys.modules[parent], parent_name, module)
|
||||||
|
|
||||||
code = compile(content, name, "exec")
|
code = compile(content, name, "exec")
|
||||||
exec(code, module.__dict__)
|
exec(code, module.__dict__) # nosec
|
||||||
sys.modules[name] = module
|
sys.modules[name] = module
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
@ -28,9 +30,12 @@ while 1:
|
|||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
sys.stdout.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
|
sshuttle.helpers.verbose = verbosity
|
||||||
|
|
||||||
import sshuttle.cmdline_options as options
|
import sshuttle.cmdline_options as options # noqa: E402
|
||||||
from sshuttle.server import main
|
from sshuttle.server import main # noqa: E402
|
||||||
main(options.latency_control)
|
main(options.latency_control, options.auto_hosts, options.to_nameserver,
|
||||||
|
options.auto_nets)
|
||||||
|
@ -1,22 +1,41 @@
|
|||||||
import socket
|
|
||||||
import errno
|
import errno
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
import sshuttle.helpers as helpers
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
|
||||||
|
import sshuttle.helpers as helpers
|
||||||
import sshuttle.ssnet as ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
import sshuttle.ssh as ssh
|
import sshuttle.ssh as ssh
|
||||||
import sshuttle.ssyslog as ssyslog
|
import sshuttle.ssyslog as ssyslog
|
||||||
import sys
|
import sshuttle.sdnotify as sdnotify
|
||||||
import platform
|
|
||||||
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
||||||
resolvconf_nameservers
|
resolvconf_nameservers
|
||||||
from sshuttle.methods import get_method, Features
|
from sshuttle.methods import get_method, Features
|
||||||
|
try:
|
||||||
|
from pwd import getpwnam
|
||||||
|
except ImportError:
|
||||||
|
getpwnam = None
|
||||||
|
|
||||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
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
|
||||||
|
|
||||||
|
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
||||||
|
|
||||||
|
|
||||||
def got_signal(signum, frame):
|
def got_signal(signum, frame):
|
||||||
@ -76,7 +95,7 @@ def daemonize():
|
|||||||
# be deleted.
|
# be deleted.
|
||||||
signal.signal(signal.SIGTERM, got_signal)
|
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(), 0)
|
||||||
os.dup2(si.fileno(), 1)
|
os.dup2(si.fileno(), 1)
|
||||||
si.close()
|
si.close()
|
||||||
@ -94,17 +113,22 @@ def daemon_cleanup():
|
|||||||
|
|
||||||
class MultiListener:
|
class MultiListener:
|
||||||
|
|
||||||
def __init__(self, type=socket.SOCK_STREAM, proto=0):
|
def __init__(self, kind=socket.SOCK_STREAM, proto=0):
|
||||||
self.v6 = socket.socket(socket.AF_INET6, type, proto)
|
self.type = kind
|
||||||
self.v4 = socket.socket(socket.AF_INET, type, proto)
|
self.proto = proto
|
||||||
|
self.v6 = None
|
||||||
|
self.v4 = None
|
||||||
|
self.bind_called = False
|
||||||
|
|
||||||
def setsockopt(self, level, optname, value):
|
def setsockopt(self, level, optname, value):
|
||||||
|
assert(self.bind_called)
|
||||||
if self.v6:
|
if self.v6:
|
||||||
self.v6.setsockopt(level, optname, value)
|
self.v6.setsockopt(level, optname, value)
|
||||||
if self.v4:
|
if self.v4:
|
||||||
self.v4.setsockopt(level, optname, value)
|
self.v4.setsockopt(level, optname, value)
|
||||||
|
|
||||||
def add_handler(self, handlers, callback, method, mux):
|
def add_handler(self, handlers, callback, method, mux):
|
||||||
|
assert(self.bind_called)
|
||||||
socks = []
|
socks = []
|
||||||
if self.v6:
|
if self.v6:
|
||||||
socks.append(self.v6)
|
socks.append(self.v6)
|
||||||
@ -119,6 +143,7 @@ class MultiListener:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def listen(self, backlog):
|
def listen(self, backlog):
|
||||||
|
assert(self.bind_called)
|
||||||
if self.v6:
|
if self.v6:
|
||||||
self.v6.listen(backlog)
|
self.v6.listen(backlog)
|
||||||
if self.v4:
|
if self.v4:
|
||||||
@ -133,16 +158,21 @@ class MultiListener:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
def bind(self, address_v6, address_v4):
|
def bind(self, address_v6, address_v4):
|
||||||
if address_v6 and self.v6:
|
assert(not self.bind_called)
|
||||||
|
self.bind_called = True
|
||||||
|
if address_v6 is not None:
|
||||||
|
self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto)
|
||||||
self.v6.bind(address_v6)
|
self.v6.bind(address_v6)
|
||||||
else:
|
else:
|
||||||
self.v6 = None
|
self.v6 = None
|
||||||
if address_v4 and self.v4:
|
if address_v4 is not None:
|
||||||
|
self.v4 = socket.socket(socket.AF_INET, self.type, self.proto)
|
||||||
self.v4.bind(address_v4)
|
self.v4.bind(address_v4)
|
||||||
else:
|
else:
|
||||||
self.v4 = None
|
self.v4 = None
|
||||||
|
|
||||||
def print_listening(self, what):
|
def print_listening(self, what):
|
||||||
|
assert(self.bind_called)
|
||||||
if self.v6:
|
if self.v6:
|
||||||
listenip = self.v6.getsockname()
|
listenip = self.v6.getsockname()
|
||||||
debug1('%s listening on %r.\n' % (what, listenip))
|
debug1('%s listening on %r.\n' % (what, listenip))
|
||||||
@ -155,7 +185,7 @@ class MultiListener:
|
|||||||
|
|
||||||
class FirewallClient:
|
class FirewallClient:
|
||||||
|
|
||||||
def __init__(self, method_name):
|
def __init__(self, method_name, sudo_pythonpath):
|
||||||
self.auto_nets = []
|
self.auto_nets = []
|
||||||
python_path = os.path.dirname(os.path.dirname(__file__))
|
python_path = os.path.dirname(os.path.dirname(__file__))
|
||||||
argvbase = ([sys.executable, sys.argv[0]] +
|
argvbase = ([sys.executable, sys.argv[0]] +
|
||||||
@ -164,11 +194,15 @@ class FirewallClient:
|
|||||||
['--firewall'])
|
['--firewall'])
|
||||||
if ssyslog._p:
|
if ssyslog._p:
|
||||||
argvbase += ['--syslog']
|
argvbase += ['--syslog']
|
||||||
argv_tries = [
|
# Default to sudo unless on OpenBSD in which case use built in `doas`
|
||||||
['sudo', '-p', '[local sudo] Password: ',
|
if platform.platform().startswith('OpenBSD'):
|
||||||
('PYTHONPATH=%s' % python_path), '--'] + argvbase,
|
elev_prefix = ['doas']
|
||||||
argvbase
|
else:
|
||||||
]
|
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
|
||||||
|
if sudo_pythonpath:
|
||||||
|
elev_prefix += ['/usr/bin/env',
|
||||||
|
'PYTHONPATH=%s' % python_path]
|
||||||
|
argv_tries = [elev_prefix + argvbase, argvbase]
|
||||||
|
|
||||||
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
|
# 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.
|
# because stupid Linux 'su' requires that stdin be attached to a tty.
|
||||||
@ -187,18 +221,14 @@ class FirewallClient:
|
|||||||
if argv[0] == 'su':
|
if argv[0] == 'su':
|
||||||
sys.stderr.write('[local su] ')
|
sys.stderr.write('[local su] ')
|
||||||
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
||||||
|
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
||||||
e = None
|
e = None
|
||||||
break
|
break
|
||||||
except OSError as e:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
self.argv = argv
|
self.argv = argv
|
||||||
s1.close()
|
s1.close()
|
||||||
if sys.version_info < (3, 0):
|
self.pfile = s2.makefile('rwb')
|
||||||
# python 2.7
|
|
||||||
self.pfile = s2.makefile('wb+')
|
|
||||||
else:
|
|
||||||
# python 3.5
|
|
||||||
self.pfile = s2.makefile('rwb')
|
|
||||||
if e:
|
if e:
|
||||||
log('Spawning firewall manager: %r\n' % self.argv)
|
log('Spawning firewall manager: %r\n' % self.argv)
|
||||||
raise Fatal(e)
|
raise Fatal(e)
|
||||||
@ -211,7 +241,8 @@ class FirewallClient:
|
|||||||
self.method.set_firewall(self)
|
self.method.set_firewall(self)
|
||||||
|
|
||||||
def setup(self, subnets_include, subnets_exclude, nslist,
|
def setup(self, subnets_include, subnets_exclude, nslist,
|
||||||
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp):
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
|
||||||
|
user):
|
||||||
self.subnets_include = subnets_include
|
self.subnets_include = subnets_include
|
||||||
self.subnets_exclude = subnets_exclude
|
self.subnets_exclude = subnets_exclude
|
||||||
self.nslist = nslist
|
self.nslist = nslist
|
||||||
@ -220,6 +251,7 @@ class FirewallClient:
|
|||||||
self.dnsport_v6 = dnsport_v6
|
self.dnsport_v6 = dnsport_v6
|
||||||
self.dnsport_v4 = dnsport_v4
|
self.dnsport_v4 = dnsport_v4
|
||||||
self.udp = udp
|
self.udp = udp
|
||||||
|
self.user = user
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
rv = self.p.poll()
|
rv = self.p.poll()
|
||||||
@ -228,12 +260,15 @@ class FirewallClient:
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.pfile.write(b'ROUTES\n')
|
self.pfile.write(b'ROUTES\n')
|
||||||
for (family, ip, width) in self.subnets_include + self.auto_nets:
|
for (family, ip, width, fport, lport) \
|
||||||
self.pfile.write(b'%d,%d,0,%s\n'
|
in self.subnets_include + self.auto_nets:
|
||||||
% (family, width, ip.encode("ASCII")))
|
self.pfile.write(b'%d,%d,0,%s,%d,%d\n' % (family, width,
|
||||||
for (family, ip, width) in self.subnets_exclude:
|
ip.encode("ASCII"),
|
||||||
self.pfile.write(b'%d,%d,1,%s\n'
|
fport, lport))
|
||||||
% (family, width, ip.encode("ASCII")))
|
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'NSLIST\n')
|
self.pfile.write(b'NSLIST\n')
|
||||||
for (family, ip) in self.nslist:
|
for (family, ip) in self.nslist:
|
||||||
@ -248,8 +283,14 @@ class FirewallClient:
|
|||||||
udp = 0
|
udp = 0
|
||||||
if self.udp:
|
if self.udp:
|
||||||
udp = 1
|
udp = 1
|
||||||
|
if self.user is None:
|
||||||
|
user = b'-'
|
||||||
|
elif isinstance(self.user, str):
|
||||||
|
user = bytes(self.user, 'utf-8')
|
||||||
|
else:
|
||||||
|
user = b'%d' % self.user
|
||||||
|
|
||||||
self.pfile.write(b'GO %d\n' % udp)
|
self.pfile.write(b'GO %d %s\n' % (udp, user))
|
||||||
self.pfile.flush()
|
self.pfile.flush()
|
||||||
|
|
||||||
line = self.pfile.readline()
|
line = self.pfile.readline()
|
||||||
@ -258,8 +299,8 @@ class FirewallClient:
|
|||||||
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
|
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
|
||||||
|
|
||||||
def sethostip(self, hostname, ip):
|
def sethostip(self, hostname, ip):
|
||||||
assert(not re.search(b'[^-\w]', hostname))
|
assert(not re.search(rb'[^-\w\.]', hostname))
|
||||||
assert(not re.search(b'[^0-9.]', ip))
|
assert(not re.search(rb'[^0-9.]', ip))
|
||||||
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
|
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
|
||||||
self.pfile.flush()
|
self.pfile.flush()
|
||||||
|
|
||||||
@ -310,7 +351,7 @@ def onaccept_tcp(listener, method, mux, handlers):
|
|||||||
sock, srcip = listener.accept()
|
sock, srcip = listener.accept()
|
||||||
sock.close()
|
sock.close()
|
||||||
finally:
|
finally:
|
||||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
@ -349,7 +390,7 @@ def onaccept_udp(listener, method, mux, handlers):
|
|||||||
srcip, dstip, data = t
|
srcip, dstip, data = t
|
||||||
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
|
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
|
||||||
if srcip in udp_by_src:
|
if srcip in udp_by_src:
|
||||||
chan, timeout = udp_by_src[srcip]
|
chan, _ = udp_by_src[srcip]
|
||||||
else:
|
else:
|
||||||
chan = mux.next_channel()
|
chan = mux.next_channel()
|
||||||
mux.channels[chan] = lambda cmd, data: udp_done(
|
mux.channels[chan] = lambda cmd, data: udp_done(
|
||||||
@ -387,7 +428,8 @@ def ondns(listener, method, mux, handlers):
|
|||||||
|
|
||||||
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control,
|
python, latency_control,
|
||||||
dns_listener, seed_hosts, auto_nets, daemon):
|
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
|
||||||
|
to_nameserver):
|
||||||
|
|
||||||
debug1('Starting client with Python version %s\n'
|
debug1('Starting client with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
@ -405,13 +447,16 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
(serverproc, serversock) = ssh.connect(
|
(serverproc, serversock) = ssh.connect(
|
||||||
ssh_cmd, remotename, python,
|
ssh_cmd, remotename, python,
|
||||||
stderr=ssyslog._p and ssyslog._p.stdin,
|
stderr=ssyslog._p and ssyslog._p.stdin,
|
||||||
options=dict(latency_control=latency_control))
|
options=dict(latency_control=latency_control,
|
||||||
|
auto_hosts=auto_hosts,
|
||||||
|
to_nameserver=to_nameserver,
|
||||||
|
auto_nets=auto_nets))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.EPIPE:
|
if e.args[0] == errno.EPIPE:
|
||||||
raise Fatal("failed to establish ssh session (1)")
|
raise Fatal("failed to establish ssh session (1)")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
mux = Mux(serversock, serversock)
|
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
|
|
||||||
expected = b'SSHUTTLE0001'
|
expected = b'SSHUTTLE0001'
|
||||||
@ -446,6 +491,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
def onroutes(routestr):
|
def onroutes(routestr):
|
||||||
if auto_nets:
|
if auto_nets:
|
||||||
for line in routestr.strip().split(b'\n'):
|
for line in routestr.strip().split(b'\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
(family, ip, width) = line.split(b',', 2)
|
(family, ip, width) = line.split(b',', 2)
|
||||||
family = int(family)
|
family = int(family)
|
||||||
width = int(width)
|
width = int(width)
|
||||||
@ -456,7 +503,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
|
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
|
||||||
else:
|
else:
|
||||||
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width))
|
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width))
|
||||||
fw.auto_nets.append((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
|
# we definitely want to do this *after* starting ssh, or we might end
|
||||||
# up intercepting the ssh connection!
|
# up intercepting the ssh connection!
|
||||||
@ -466,9 +513,14 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
# set --auto-nets, we might as well wait for the message first, then
|
# set --auto-nets, we might as well wait for the message first, then
|
||||||
# ignore its contents.
|
# ignore its contents.
|
||||||
mux.got_routes = None
|
mux.got_routes = None
|
||||||
fw.start()
|
serverready()
|
||||||
|
|
||||||
mux.got_routes = onroutes
|
mux.got_routes = onroutes
|
||||||
|
|
||||||
|
def serverready():
|
||||||
|
fw.start()
|
||||||
|
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
|
||||||
|
|
||||||
def onhostlist(hostlist):
|
def onhostlist(hostlist):
|
||||||
debug2('got host list: %r\n' % hostlist)
|
debug2('got host list: %r\n' % hostlist)
|
||||||
for line in hostlist.strip().split():
|
for line in hostlist.strip().split():
|
||||||
@ -487,7 +539,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
|
|
||||||
if seed_hosts is not None:
|
if seed_hosts is not None:
|
||||||
debug1('seed_hosts: %r\n' % seed_hosts)
|
debug1('seed_hosts: %r\n' % seed_hosts)
|
||||||
mux.send(0, ssnet.CMD_HOST_REQ, b'\n'.join(seed_hosts))
|
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
rv = serverproc.poll()
|
rv = serverproc.poll()
|
||||||
@ -501,8 +553,9 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
|
|
||||||
def main(listenip_v6, listenip_v4,
|
def main(listenip_v6, listenip_v4,
|
||||||
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
||||||
method_name, seed_hosts, auto_nets,
|
method_name, seed_hosts, auto_hosts, auto_nets,
|
||||||
subnets_include, subnets_exclude, daemon, pidfile):
|
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
||||||
|
user, sudo_pythonpath):
|
||||||
|
|
||||||
if daemon:
|
if daemon:
|
||||||
try:
|
try:
|
||||||
@ -512,11 +565,16 @@ def main(listenip_v6, listenip_v4,
|
|||||||
return 5
|
return 5
|
||||||
debug1('Starting sshuttle proxy.\n')
|
debug1('Starting sshuttle proxy.\n')
|
||||||
|
|
||||||
fw = FirewallClient(method_name)
|
fw = FirewallClient(method_name, sudo_pythonpath)
|
||||||
|
|
||||||
# Get family specific subnet lists
|
# Get family specific subnet lists
|
||||||
if dns:
|
if dns:
|
||||||
nslist += resolvconf_nameservers()
|
nslist += resolvconf_nameservers()
|
||||||
|
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
|
||||||
|
to_nameserver = None
|
||||||
|
|
||||||
subnets = subnets_include + subnets_exclude # we don't care here
|
subnets = subnets_include + subnets_exclude # we don't care here
|
||||||
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
||||||
@ -534,10 +592,29 @@ def main(listenip_v6, listenip_v4,
|
|||||||
else:
|
else:
|
||||||
listenip_v6 = None
|
listenip_v6 = None
|
||||||
|
|
||||||
required.ipv6 = len(subnets_v6) > 0 or len(nslist_v6) > 0 \
|
if user is not None:
|
||||||
or listenip_v6 is not None
|
if getpwnam is None:
|
||||||
|
raise Fatal("Routing by user not available on this system.")
|
||||||
|
try:
|
||||||
|
user = getpwnam(user).pw_uid
|
||||||
|
except KeyError:
|
||||||
|
raise Fatal("User %s does not exist." % user)
|
||||||
|
|
||||||
|
if fw.method.name != 'nat':
|
||||||
|
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
|
||||||
|
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
|
||||||
|
else:
|
||||||
|
required.ipv6 = None
|
||||||
|
required.ipv4 = None
|
||||||
|
|
||||||
required.udp = avail.udp
|
required.udp = avail.udp
|
||||||
required.dns = len(nslist) > 0
|
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:
|
||||||
|
nslist_v6 = []
|
||||||
|
nslist = nslist_v4
|
||||||
|
|
||||||
fw.method.assert_features(required)
|
fw.method.assert_features(required)
|
||||||
|
|
||||||
@ -548,17 +625,29 @@ def main(listenip_v6, listenip_v4,
|
|||||||
debug1("IPv6 enabled: %r\n" % required.ipv6)
|
debug1("IPv6 enabled: %r\n" % required.ipv6)
|
||||||
debug1("UDP enabled: %r\n" % required.udp)
|
debug1("UDP enabled: %r\n" % required.udp)
|
||||||
debug1("DNS enabled: %r\n" % required.dns)
|
debug1("DNS enabled: %r\n" % required.dns)
|
||||||
|
debug1("User enabled: %r\n" % required.user)
|
||||||
|
|
||||||
# bind to required ports
|
# bind to required ports
|
||||||
if listenip_v4 == "auto":
|
if listenip_v4 == "auto":
|
||||||
listenip_v4 = ('127.0.0.1', 0)
|
listenip_v4 = ('127.0.0.1', 0)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
if required.ipv6 and \
|
||||||
|
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
|
||||||
|
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
|
||||||
|
|
||||||
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
||||||
# if both ports given, no need to search for a spare port
|
# if both ports given, no need to search for a spare port
|
||||||
ports = [0, ]
|
ports = [0, ]
|
||||||
else:
|
else:
|
||||||
# if at least one port missing, we have to search
|
# if at least one port missing, we have to search
|
||||||
ports = range(12300, 9000, -1)
|
ports = range(12300, 9000, -1)
|
||||||
|
# keep track of failed bindings and used ports to avoid trying to
|
||||||
|
# bind to the same socket address twice in different listeners
|
||||||
|
used_ports = []
|
||||||
|
|
||||||
# search for free ports and try to bind
|
# search for free ports and try to bind
|
||||||
last_e = None
|
last_e = None
|
||||||
@ -569,11 +658,9 @@ def main(listenip_v6, listenip_v4,
|
|||||||
for port in ports:
|
for port in ports:
|
||||||
debug2(' %d' % port)
|
debug2(' %d' % port)
|
||||||
tcp_listener = MultiListener()
|
tcp_listener = MultiListener()
|
||||||
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
|
|
||||||
if required.udp:
|
if required.udp:
|
||||||
udp_listener = MultiListener(socket.SOCK_DGRAM)
|
udp_listener = MultiListener(socket.SOCK_DGRAM)
|
||||||
udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
else:
|
else:
|
||||||
udp_listener = None
|
udp_listener = None
|
||||||
|
|
||||||
@ -602,10 +689,12 @@ def main(listenip_v6, listenip_v4,
|
|||||||
if udp_listener:
|
if udp_listener:
|
||||||
udp_listener.bind(lv6, lv4)
|
udp_listener.bind(lv6, lv4)
|
||||||
bound = True
|
bound = True
|
||||||
|
used_ports.append(port)
|
||||||
break
|
break
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.errno == errno.EADDRINUSE:
|
if e.errno == errno.EADDRINUSE:
|
||||||
last_e = e
|
last_e = e
|
||||||
|
used_ports.append(port)
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@ -625,6 +714,9 @@ def main(listenip_v6, listenip_v4,
|
|||||||
ports = range(12300, 9000, -1)
|
ports = range(12300, 9000, -1)
|
||||||
for port in ports:
|
for port in ports:
|
||||||
debug2(' %d' % port)
|
debug2(' %d' % port)
|
||||||
|
if port in used_ports:
|
||||||
|
continue
|
||||||
|
|
||||||
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
||||||
|
|
||||||
if listenip_v6:
|
if listenip_v6:
|
||||||
@ -644,10 +736,12 @@ def main(listenip_v6, listenip_v4,
|
|||||||
try:
|
try:
|
||||||
dns_listener.bind(lv6, lv4)
|
dns_listener.bind(lv6, lv4)
|
||||||
bound = True
|
bound = True
|
||||||
|
used_ports.append(port)
|
||||||
break
|
break
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.errno == errno.EADDRINUSE:
|
if e.errno == errno.EADDRINUSE:
|
||||||
last_e = e
|
last_e = e
|
||||||
|
used_ports.append(port)
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
debug2('\n')
|
debug2('\n')
|
||||||
@ -663,22 +757,22 @@ def main(listenip_v6, listenip_v4,
|
|||||||
# Last minute sanity checks.
|
# Last minute sanity checks.
|
||||||
# These should never fail.
|
# These should never fail.
|
||||||
# If these do fail, something is broken above.
|
# If these do fail, something is broken above.
|
||||||
if len(subnets_v6) > 0:
|
if subnets_v6:
|
||||||
assert required.ipv6
|
assert required.ipv6
|
||||||
if redirectport_v6 == 0:
|
if redirectport_v6 == 0:
|
||||||
raise Fatal("IPv6 subnets defined but not listening")
|
raise Fatal("IPv6 subnets defined but not listening")
|
||||||
|
|
||||||
if len(nslist_v6) > 0:
|
if nslist_v6:
|
||||||
assert required.dns
|
assert required.dns
|
||||||
assert required.ipv6
|
assert required.ipv6
|
||||||
if dnsport_v6 == 0:
|
if dnsport_v6 == 0:
|
||||||
raise Fatal("IPv6 ns servers defined but not listening")
|
raise Fatal("IPv6 ns servers defined but not listening")
|
||||||
|
|
||||||
if len(subnets_v4) > 0:
|
if subnets_v4:
|
||||||
if redirectport_v4 == 0:
|
if redirectport_v4 == 0:
|
||||||
raise Fatal("IPv4 subnets defined but not listening")
|
raise Fatal("IPv4 subnets defined but not listening")
|
||||||
|
|
||||||
if len(nslist_v4) > 0:
|
if nslist_v4:
|
||||||
if dnsport_v4 == 0:
|
if dnsport_v4 == 0:
|
||||||
raise Fatal("IPv4 ns servers defined but not listening")
|
raise Fatal("IPv4 ns servers defined but not listening")
|
||||||
|
|
||||||
@ -692,19 +786,21 @@ def main(listenip_v6, listenip_v4,
|
|||||||
# start the firewall
|
# start the firewall
|
||||||
fw.setup(subnets_include, subnets_exclude, nslist,
|
fw.setup(subnets_include, subnets_exclude, nslist,
|
||||||
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
|
||||||
required.udp)
|
required.udp, user)
|
||||||
|
|
||||||
# start the client process
|
# start the client process
|
||||||
try:
|
try:
|
||||||
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control, dns_listener,
|
python, latency_control, dns_listener,
|
||||||
seed_hosts, auto_nets, daemon)
|
seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
if daemon:
|
if daemon:
|
||||||
# it's not our child anymore; can't waitpid
|
# it's not our child anymore; can't waitpid
|
||||||
fw.p.returncode = 0
|
fw.p.returncode = 0
|
||||||
fw.done()
|
fw.done()
|
||||||
|
sdnotify.send(sdnotify.stop())
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if daemon:
|
if daemon:
|
||||||
daemon_cleanup()
|
daemon_cleanup()
|
||||||
|
116
sshuttle/cmdline.py
Normal file
116
sshuttle/cmdline.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import platform
|
||||||
|
import sshuttle.helpers as helpers
|
||||||
|
import sshuttle.client as client
|
||||||
|
import sshuttle.firewall as firewall
|
||||||
|
import sshuttle.hostwatch as hostwatch
|
||||||
|
import sshuttle.ssyslog as ssyslog
|
||||||
|
from sshuttle.options import parser, parse_ipport
|
||||||
|
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
||||||
|
from sshuttle.sudoers import sudoers
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
opt = parser.parse_args()
|
||||||
|
|
||||||
|
if opt.sudoers or opt.sudoers_no_modify:
|
||||||
|
if platform.platform().startswith('OpenBSD'):
|
||||||
|
log('Automatic sudoers does not work on BSD')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
if not opt.sudoers_filename:
|
||||||
|
log('--sudoers-file must be set or omited.')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
sudoers(
|
||||||
|
user_name=opt.sudoers_user,
|
||||||
|
no_modify=opt.sudoers_no_modify,
|
||||||
|
file_name=opt.sudoers_filename
|
||||||
|
)
|
||||||
|
|
||||||
|
if opt.daemon:
|
||||||
|
opt.syslog = 1
|
||||||
|
if opt.wrap:
|
||||||
|
import sshuttle.ssnet as ssnet
|
||||||
|
ssnet.MAX_CHANNEL = opt.wrap
|
||||||
|
if opt.latency_buffer_size:
|
||||||
|
import sshuttle.ssnet as ssnet
|
||||||
|
ssnet.LATENCY_BUFFER_SIZE = opt.latency_buffer_size
|
||||||
|
helpers.verbose = opt.verbose
|
||||||
|
|
||||||
|
try:
|
||||||
|
if opt.firewall:
|
||||||
|
if opt.subnets or opt.subnets_file:
|
||||||
|
parser.error('exactly zero arguments expected')
|
||||||
|
return firewall.main(opt.method, opt.syslog)
|
||||||
|
elif opt.hostwatch:
|
||||||
|
return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
|
||||||
|
else:
|
||||||
|
includes = opt.subnets + opt.subnets_file
|
||||||
|
excludes = opt.exclude
|
||||||
|
if not includes and not opt.auto_nets:
|
||||||
|
parser.error('at least one subnet, subnet file, '
|
||||||
|
'or -N expected')
|
||||||
|
remotename = opt.remote
|
||||||
|
if remotename == '' or remotename == '-':
|
||||||
|
remotename = None
|
||||||
|
nslist = [family_ip_tuple(ns) for ns in opt.ns_hosts]
|
||||||
|
if opt.seed_hosts:
|
||||||
|
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
|
||||||
|
elif opt.auto_hosts:
|
||||||
|
sh = []
|
||||||
|
else:
|
||||||
|
sh = None
|
||||||
|
if opt.listen:
|
||||||
|
ipport_v6 = None
|
||||||
|
ipport_v4 = None
|
||||||
|
lst = opt.listen.split(",")
|
||||||
|
for ip in lst:
|
||||||
|
family, ip, port = parse_ipport(ip)
|
||||||
|
if family == socket.AF_INET6:
|
||||||
|
ipport_v6 = (ip, port)
|
||||||
|
else:
|
||||||
|
ipport_v4 = (ip, port)
|
||||||
|
else:
|
||||||
|
# parse_ipport4('127.0.0.1:0')
|
||||||
|
ipport_v4 = "auto"
|
||||||
|
# parse_ipport6('[::1]:0')
|
||||||
|
ipport_v6 = "auto" if not opt.disable_ipv6 else None
|
||||||
|
if opt.syslog:
|
||||||
|
ssyslog.start_syslog()
|
||||||
|
ssyslog.close_stdin()
|
||||||
|
ssyslog.stdout_to_syslog()
|
||||||
|
ssyslog.stderr_to_syslog()
|
||||||
|
return_code = client.main(ipport_v6, ipport_v4,
|
||||||
|
opt.ssh_cmd,
|
||||||
|
remotename,
|
||||||
|
opt.python,
|
||||||
|
opt.latency_control,
|
||||||
|
opt.dns,
|
||||||
|
nslist,
|
||||||
|
opt.method,
|
||||||
|
sh,
|
||||||
|
opt.auto_hosts,
|
||||||
|
opt.auto_nets,
|
||||||
|
includes,
|
||||||
|
excludes,
|
||||||
|
opt.daemon,
|
||||||
|
opt.to_ns,
|
||||||
|
opt.pidfile,
|
||||||
|
opt.user,
|
||||||
|
opt.sudo_pythonpath)
|
||||||
|
|
||||||
|
if return_code == 0:
|
||||||
|
log('Normal exit code, exiting...')
|
||||||
|
else:
|
||||||
|
log('Abnormal exit code %d detected, failing...' % return_code)
|
||||||
|
return return_code
|
||||||
|
|
||||||
|
except Fatal as e:
|
||||||
|
log('fatal: %s\n' % e)
|
||||||
|
return 99
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log('\n')
|
||||||
|
log('Keyboard interrupt: exiting.\n')
|
||||||
|
return 1
|
@ -1,11 +1,12 @@
|
|||||||
import errno
|
import errno
|
||||||
import socket
|
import socket
|
||||||
import signal
|
import signal
|
||||||
import sshuttle.ssyslog as ssyslog
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
import sshuttle.ssyslog as ssyslog
|
||||||
from sshuttle.helpers import debug1, debug2, Fatal
|
from sshuttle.helpers import debug1, debug2, Fatal
|
||||||
from sshuttle.methods import get_auto_method, get_method
|
from sshuttle.methods import get_auto_method, get_method
|
||||||
|
|
||||||
@ -74,6 +75,16 @@ def setup_daemon():
|
|||||||
return sys.stdin, sys.stdout
|
return sys.stdin, sys.stdout
|
||||||
|
|
||||||
|
|
||||||
|
# Note that we're sorting in a very particular order:
|
||||||
|
# we need to go from smaller, more specific, port ranges, to larger,
|
||||||
|
# less-specific, port ranges. At each level, we order by subnet
|
||||||
|
# width, from most-specific subnets (largest swidth) to
|
||||||
|
# least-specific. On ties, excludes come first.
|
||||||
|
# s:(inet, subnet width, exclude flag, subnet, first port, last port)
|
||||||
|
def subnet_weight(s):
|
||||||
|
return (-s[-1] + (s[-2] or -65535), s[1], s[2])
|
||||||
|
|
||||||
|
|
||||||
# This is some voodoo for setting up the kernel's transparent
|
# This is some voodoo for setting up the kernel's transparent
|
||||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||||
# otherwise we delete it, then make them from scratch.
|
# otherwise we delete it, then make them from scratch.
|
||||||
@ -119,10 +130,17 @@ def main(method_name, syslog):
|
|||||||
elif line.startswith("NSLIST\n"):
|
elif line.startswith("NSLIST\n"):
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, width, exclude, ip) = line.strip().split(',', 3)
|
(family, width, exclude, ip, fport, lport) = \
|
||||||
except:
|
line.strip().split(',', 5)
|
||||||
|
except BaseException:
|
||||||
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
|
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
|
||||||
subnets.append((int(family), int(width), bool(int(exclude)), ip))
|
subnets.append((
|
||||||
|
int(family),
|
||||||
|
int(width),
|
||||||
|
bool(int(exclude)),
|
||||||
|
ip,
|
||||||
|
int(fport),
|
||||||
|
int(lport)))
|
||||||
debug2('firewall manager: Got subnets: %r\n' % subnets)
|
debug2('firewall manager: Got subnets: %r\n' % subnets)
|
||||||
|
|
||||||
nslist = []
|
nslist = []
|
||||||
@ -136,7 +154,7 @@ def main(method_name, syslog):
|
|||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
(family, ip) = line.strip().split(',', 1)
|
(family, ip) = line.strip().split(',', 1)
|
||||||
except:
|
except BaseException:
|
||||||
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
|
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
|
||||||
nslist.append((int(family), ip))
|
nslist.append((int(family), ip))
|
||||||
debug2('firewall manager: Got partial nslist: %r\n' % nslist)
|
debug2('firewall manager: Got partial nslist: %r\n' % nslist)
|
||||||
@ -147,7 +165,7 @@ def main(method_name, syslog):
|
|||||||
_, _, ports = line.partition(" ")
|
_, _, ports = line.partition(" ")
|
||||||
ports = ports.split(",")
|
ports = ports.split(",")
|
||||||
if len(ports) != 4:
|
if len(ports) != 4:
|
||||||
raise Fatal('firewall: expected 4 ports but got %n' % len(ports))
|
raise Fatal('firewall: expected 4 ports but got %d' % len(ports))
|
||||||
port_v6 = int(ports[0])
|
port_v6 = int(ports[0])
|
||||||
port_v4 = int(ports[1])
|
port_v4 = int(ports[1])
|
||||||
dnsport_v6 = int(ports[2])
|
dnsport_v6 = int(ports[2])
|
||||||
@ -171,9 +189,12 @@ def main(method_name, syslog):
|
|||||||
elif not line.startswith("GO "):
|
elif not line.startswith("GO "):
|
||||||
raise Fatal('firewall: expected GO but got %r' % line)
|
raise Fatal('firewall: expected GO but got %r' % line)
|
||||||
|
|
||||||
_, _, udp = line.partition(" ")
|
_, _, args = line.partition(" ")
|
||||||
|
udp, user = args.strip().split(" ", 1)
|
||||||
udp = bool(int(udp))
|
udp = bool(int(udp))
|
||||||
debug2('firewall manager: Got udp: %r\n' % udp)
|
if user == '-':
|
||||||
|
user = None
|
||||||
|
debug2('firewall manager: Got udp: %r, user: %r\n' % (udp, user))
|
||||||
|
|
||||||
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
||||||
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
||||||
@ -183,17 +204,19 @@ def main(method_name, syslog):
|
|||||||
try:
|
try:
|
||||||
debug1('firewall manager: setting up.\n')
|
debug1('firewall manager: setting up.\n')
|
||||||
|
|
||||||
if len(subnets_v6) > 0 or len(nslist_v6) > 0:
|
if subnets_v6 or nslist_v6:
|
||||||
debug2('firewall manager: setting up IPv6.\n')
|
debug2('firewall manager: setting up IPv6.\n')
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
port_v6, dnsport_v6, nslist_v6,
|
port_v6, dnsport_v6, nslist_v6,
|
||||||
socket.AF_INET6, subnets_v6, udp)
|
socket.AF_INET6, subnets_v6, udp,
|
||||||
|
user)
|
||||||
|
|
||||||
if len(subnets_v4) > 0 or len(nslist_v4) > 0:
|
if subnets_v4 or nslist_v4:
|
||||||
debug2('firewall manager: setting up IPv4.\n')
|
debug2('firewall manager: setting up IPv4.\n')
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
port_v4, dnsport_v4, nslist_v4,
|
port_v4, dnsport_v4, nslist_v4,
|
||||||
socket.AF_INET, subnets_v4, udp)
|
socket.AF_INET, subnets_v4, udp,
|
||||||
|
user)
|
||||||
|
|
||||||
stdout.write('STARTED\n')
|
stdout.write('STARTED\n')
|
||||||
|
|
||||||
@ -222,43 +245,43 @@ def main(method_name, syslog):
|
|||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
debug1('firewall manager: undoing changes.\n')
|
debug1('firewall manager: undoing changes.\n')
|
||||||
except:
|
except BaseException:
|
||||||
pass
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if len(subnets_v6) > 0 or len(nslist_v6) > 0:
|
if subnets_v6 or nslist_v6:
|
||||||
debug2('firewall manager: undoing IPv6 changes.\n')
|
debug2('firewall manager: undoing IPv6 changes.\n')
|
||||||
method.restore_firewall(port_v6, socket.AF_INET6, udp)
|
method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
|
||||||
except:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("firewall manager: "
|
debug1("firewall manager: "
|
||||||
"Error trying to undo IPv6 firewall.\n")
|
"Error trying to undo IPv6 firewall.\n")
|
||||||
for line in traceback.format_exc().splitlines():
|
for line in traceback.format_exc().splitlines():
|
||||||
debug1("---> %s\n" % line)
|
debug1("---> %s\n" % line)
|
||||||
except:
|
except BaseException:
|
||||||
pass
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if len(subnets_v4) > 0 or len(nslist_v4) > 0:
|
if subnets_v4 or nslist_v4:
|
||||||
debug2('firewall manager: undoing IPv4 changes.\n')
|
debug2('firewall manager: undoing IPv4 changes.\n')
|
||||||
method.restore_firewall(port_v4, socket.AF_INET, udp)
|
method.restore_firewall(port_v4, socket.AF_INET, udp, user)
|
||||||
except:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("firewall manager: "
|
debug1("firewall manager: "
|
||||||
"Error trying to undo IPv4 firewall.\n")
|
"Error trying to undo IPv4 firewall.\n")
|
||||||
for line in traceback.format_exc().splitlines():
|
for line in traceback.format_exc().splitlines():
|
||||||
debug1("firewall manager: ---> %s\n" % line)
|
debug1("firewall manager: ---> %s\n" % line)
|
||||||
except:
|
except BaseException:
|
||||||
pass
|
debug2('An error occurred, ignoring it.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
debug2('firewall manager: undoing /etc/hosts changes.\n')
|
debug2('firewall manager: undoing /etc/hosts changes.\n')
|
||||||
restore_etc_hosts(port_v6 or port_v4)
|
restore_etc_hosts(port_v6 or port_v4)
|
||||||
except:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
debug1("firewall manager: "
|
debug1("firewall manager: "
|
||||||
"Error trying to undo /etc/hosts changes.\n")
|
"Error trying to undo /etc/hosts changes.\n")
|
||||||
for line in traceback.format_exc().splitlines():
|
for line in traceback.format_exc().splitlines():
|
||||||
debug1("firewall manager: ---> %s\n" % line)
|
debug1("firewall manager: ---> %s\n" % line)
|
||||||
except:
|
except BaseException:
|
||||||
pass
|
debug2('An error occurred, ignoring it.')
|
||||||
|
@ -6,6 +6,10 @@ logprefix = ''
|
|||||||
verbose = 0
|
verbose = 0
|
||||||
|
|
||||||
|
|
||||||
|
def b(s):
|
||||||
|
return s.encode("ASCII")
|
||||||
|
|
||||||
|
|
||||||
def log(s):
|
def log(s):
|
||||||
global logprefix
|
global logprefix
|
||||||
try:
|
try:
|
||||||
@ -45,22 +49,22 @@ class Fatal(Exception):
|
|||||||
|
|
||||||
|
|
||||||
def resolvconf_nameservers():
|
def resolvconf_nameservers():
|
||||||
l = []
|
lines = []
|
||||||
for line in open('/etc/resolv.conf'):
|
for line in open('/etc/resolv.conf'):
|
||||||
words = line.lower().split()
|
words = line.lower().split()
|
||||||
if len(words) >= 2 and words[0] == 'nameserver':
|
if len(words) >= 2 and words[0] == 'nameserver':
|
||||||
l.append(family_ip_tuple(words[1]))
|
lines.append(family_ip_tuple(words[1]))
|
||||||
return l
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def resolvconf_random_nameserver():
|
def resolvconf_random_nameserver():
|
||||||
l = resolvconf_nameservers()
|
lines = resolvconf_nameservers()
|
||||||
if l:
|
if lines:
|
||||||
if len(l) > 1:
|
if len(lines) > 1:
|
||||||
# don't import this unless we really need it
|
# don't import this unless we really need it
|
||||||
import random
|
import random
|
||||||
random.shuffle(l)
|
random.shuffle(lines)
|
||||||
return l[0]
|
return lines[0]
|
||||||
else:
|
else:
|
||||||
return (socket.AF_INET, '127.0.0.1')
|
return (socket.AF_INET, '127.0.0.1')
|
||||||
|
|
||||||
@ -70,7 +74,8 @@ def islocal(ip, family):
|
|||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
sock.bind((ip, 0))
|
sock.bind((ip, 0))
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.args[0] == errno.EADDRNOTAVAIL:
|
if e.args[0] == errno.EADDRNOTAVAIL:
|
||||||
return False # not a local IP
|
return False # not a local IP
|
||||||
else:
|
else:
|
||||||
|
@ -21,8 +21,9 @@ _smb_ok = True
|
|||||||
hostnames = {}
|
hostnames = {}
|
||||||
queue = {}
|
queue = {}
|
||||||
try:
|
try:
|
||||||
null = open('/dev/null', 'wb')
|
null = open(os.devnull, 'wb')
|
||||||
except IOError as e:
|
except IOError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('warning: %s\n' % e)
|
log('warning: %s\n' % e)
|
||||||
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
||||||
|
|
||||||
@ -38,19 +39,20 @@ def write_host_cache():
|
|||||||
for name, ip in sorted(hostnames.items()):
|
for name, ip in sorted(hostnames.items()):
|
||||||
f.write(('%s,%s\n' % (name, ip)).encode("ASCII"))
|
f.write(('%s,%s\n' % (name, ip)).encode("ASCII"))
|
||||||
f.close()
|
f.close()
|
||||||
os.chmod(tmpname, 0o600)
|
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
|
||||||
os.rename(tmpname, CACHEFILE)
|
os.rename(tmpname, CACHEFILE)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
os.unlink(tmpname)
|
os.unlink(tmpname)
|
||||||
except:
|
except BaseException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def read_host_cache():
|
def read_host_cache():
|
||||||
try:
|
try:
|
||||||
f = open(CACHEFILE)
|
f = open(CACHEFILE)
|
||||||
except IOError as e:
|
except IOError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@ -59,23 +61,27 @@ def read_host_cache():
|
|||||||
words = line.strip().split(',')
|
words = line.strip().split(',')
|
||||||
if len(words) == 2:
|
if len(words) == 2:
|
||||||
(name, ip) = words
|
(name, ip) = words
|
||||||
name = re.sub(r'[^-\w]', '-', name).strip()
|
name = re.sub(r'[^-\w\.]', '-', name).strip()
|
||||||
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
ip = re.sub(r'[^0-9.]', '', ip).strip()
|
||||||
if name and ip:
|
if name and ip:
|
||||||
found_host(name, ip)
|
found_host(name, ip)
|
||||||
|
|
||||||
|
|
||||||
def found_host(hostname, ip):
|
def found_host(name, ip):
|
||||||
hostname = re.sub(r'\..*', '', hostname)
|
hostname = re.sub(r'\..*', '', name)
|
||||||
hostname = re.sub(r'[^-\w]', '_', hostname)
|
hostname = re.sub(r'[^-\w\.]', '_', hostname)
|
||||||
if (ip.startswith('127.') or ip.startswith('255.')
|
if (ip.startswith('127.') or ip.startswith('255.') or
|
||||||
or hostname == 'localhost'):
|
hostname == 'localhost'):
|
||||||
return
|
return
|
||||||
oldip = hostnames.get(hostname)
|
|
||||||
|
if hostname != name:
|
||||||
|
found_host(hostname, ip)
|
||||||
|
|
||||||
|
oldip = hostnames.get(name)
|
||||||
if oldip != ip:
|
if oldip != ip:
|
||||||
hostnames[hostname] = ip
|
hostnames[name] = ip
|
||||||
debug1('Found: %s: %s\n' % (hostname, ip))
|
debug1('Found: %s: %s\n' % (name, ip))
|
||||||
sys.stdout.write('%s,%s\n' % (hostname, ip))
|
sys.stdout.write('%s,%s\n' % (name, ip))
|
||||||
write_host_cache()
|
write_host_cache()
|
||||||
|
|
||||||
|
|
||||||
@ -102,7 +108,7 @@ def _check_revdns(ip):
|
|||||||
debug3('< %s\n' % r[0])
|
debug3('< %s\n' % r[0])
|
||||||
check_host(r[0])
|
check_host(r[0])
|
||||||
found_host(r[0], ip)
|
found_host(r[0], ip)
|
||||||
except socket.herror:
|
except (socket.herror, UnicodeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -113,18 +119,24 @@ def _check_dns(hostname):
|
|||||||
debug3('< %s\n' % ip)
|
debug3('< %s\n' % ip)
|
||||||
check_host(ip)
|
check_host(ip)
|
||||||
found_host(hostname, ip)
|
found_host(hostname, ip)
|
||||||
except socket.gaierror:
|
except (socket.gaierror, UnicodeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _check_netstat():
|
def _check_netstat():
|
||||||
debug2(' > netstat\n')
|
debug2(' > netstat\n')
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
argv = ['netstat', '-n']
|
argv = ['netstat', '-n']
|
||||||
try:
|
try:
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||||
|
env=env)
|
||||||
content = p.stdout.read().decode("ASCII")
|
content = p.stdout.read().decode("ASCII")
|
||||||
p.wait()
|
p.wait()
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -138,13 +150,19 @@ def _check_smb(hostname):
|
|||||||
global _smb_ok
|
global _smb_ok
|
||||||
if not _smb_ok:
|
if not _smb_ok:
|
||||||
return
|
return
|
||||||
argv = ['smbclient', '-U', '%', '-L', hostname]
|
|
||||||
debug2(' > smb: %s\n' % hostname)
|
debug2(' > smb: %s\n' % hostname)
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
argv = ['smbclient', '-U', '%', '-L', hostname]
|
||||||
try:
|
try:
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||||
|
env=env)
|
||||||
lines = p.stdout.readlines()
|
lines = p.stdout.readlines()
|
||||||
p.wait()
|
p.wait()
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
_smb_ok = False
|
_smb_ok = False
|
||||||
return
|
return
|
||||||
@ -195,13 +213,19 @@ def _check_nmb(hostname, is_workgroup, is_master):
|
|||||||
global _nmb_ok
|
global _nmb_ok
|
||||||
if not _nmb_ok:
|
if not _nmb_ok:
|
||||||
return
|
return
|
||||||
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
|
|
||||||
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
|
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
|
||||||
try:
|
try:
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null,
|
||||||
|
env=env)
|
||||||
lines = p.stdout.readlines()
|
lines = p.stdout.readlines()
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
_nmb_ok = False
|
_nmb_ok = False
|
||||||
return
|
return
|
||||||
@ -242,7 +266,7 @@ def _enqueue(op, *args):
|
|||||||
|
|
||||||
|
|
||||||
def _stdin_still_ok(timeout):
|
def _stdin_still_ok(timeout):
|
||||||
r, w, x = select.select([sys.stdin.fileno()], [], [], timeout)
|
r, _, _ = select.select([sys.stdin.fileno()], [], [], timeout)
|
||||||
if r:
|
if r:
|
||||||
b = os.read(sys.stdin.fileno(), 4096)
|
b = os.read(sys.stdin.fileno(), 4096)
|
||||||
if not b:
|
if not b:
|
||||||
@ -250,7 +274,7 @@ def _stdin_still_ok(timeout):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def hw_main(seed_hosts):
|
def hw_main(seed_hosts, auto_hosts):
|
||||||
if helpers.verbose >= 2:
|
if helpers.verbose >= 2:
|
||||||
helpers.logprefix = 'HH: '
|
helpers.logprefix = 'HH: '
|
||||||
else:
|
else:
|
||||||
@ -259,17 +283,18 @@ def hw_main(seed_hosts):
|
|||||||
debug1('Starting hostwatch with Python version %s\n'
|
debug1('Starting hostwatch with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
read_host_cache()
|
|
||||||
|
|
||||||
_enqueue(_check_etc_hosts)
|
|
||||||
_enqueue(_check_netstat)
|
|
||||||
check_host('localhost')
|
|
||||||
check_host(socket.gethostname())
|
|
||||||
check_workgroup('workgroup')
|
|
||||||
check_workgroup('-')
|
|
||||||
for h in seed_hosts:
|
for h in seed_hosts:
|
||||||
check_host(h)
|
check_host(h)
|
||||||
|
|
||||||
|
if auto_hosts:
|
||||||
|
read_host_cache()
|
||||||
|
_enqueue(_check_etc_hosts)
|
||||||
|
_enqueue(_check_netstat)
|
||||||
|
check_host('localhost')
|
||||||
|
check_host(socket.gethostname())
|
||||||
|
check_workgroup('workgroup')
|
||||||
|
check_workgroup('-')
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for t, last_polled in list(queue.items()):
|
for t, last_polled in list(queue.items()):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
from sshuttle.helpers import log, debug1, Fatal, family_to_string
|
from sshuttle.helpers import log, debug1, Fatal, family_to_string
|
||||||
@ -18,13 +19,17 @@ def ipt_chain_exists(family, table, name):
|
|||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
argv = [cmd, '-t', table, '-nL']
|
argv = [cmd, '-t', table, '-nL']
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
env = {
|
||||||
for line in p.stdout:
|
'PATH': os.environ['PATH'],
|
||||||
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
|
'LC_ALL': "C",
|
||||||
return True
|
}
|
||||||
rv = p.wait()
|
try:
|
||||||
if rv:
|
output = ssubprocess.check_output(argv, env=env)
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
for line in output.decode('ASCII').split('\n'):
|
||||||
|
if line.startswith('Chain %s ' % name):
|
||||||
|
return True
|
||||||
|
except ssubprocess.CalledProcessError as e:
|
||||||
|
raise Fatal('%r returned %d' % (argv, e.returncode))
|
||||||
|
|
||||||
|
|
||||||
def ipt(family, table, *args):
|
def ipt(family, table, *args):
|
||||||
@ -35,7 +40,26 @@ def ipt(family, table, *args):
|
|||||||
else:
|
else:
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
rv = ssubprocess.call(argv)
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
rv = ssubprocess.call(argv, env=env)
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
def nft(family, table, action, *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)
|
||||||
if rv:
|
if rv:
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
@ -35,17 +35,21 @@ class BaseMethod(object):
|
|||||||
def set_firewall(self, firewall):
|
def set_firewall(self, firewall):
|
||||||
self.firewall = firewall
|
self.firewall = firewall
|
||||||
|
|
||||||
def get_supported_features(self):
|
@staticmethod
|
||||||
|
def get_supported_features():
|
||||||
result = Features()
|
result = Features()
|
||||||
result.ipv6 = False
|
result.ipv6 = False
|
||||||
result.udp = False
|
result.udp = False
|
||||||
result.dns = True
|
result.dns = True
|
||||||
|
result.user = False
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_tcp_dstip(self, sock):
|
@staticmethod
|
||||||
|
def get_tcp_dstip(sock):
|
||||||
return original_dst(sock)
|
return original_dst(sock)
|
||||||
|
|
||||||
def recv_udp(self, udp_listener, bufsize):
|
@staticmethod
|
||||||
|
def recv_udp(udp_listener, bufsize):
|
||||||
debug3('Accept UDP using recvfrom.\n')
|
debug3('Accept UDP using recvfrom.\n')
|
||||||
data, srcip = udp_listener.recvfrom(bufsize)
|
data, srcip = udp_listener.recvfrom(bufsize)
|
||||||
return (srcip, None, data)
|
return (srcip, None, data)
|
||||||
@ -64,19 +68,21 @@ class BaseMethod(object):
|
|||||||
|
|
||||||
def assert_features(self, features):
|
def assert_features(self, features):
|
||||||
avail = self.get_supported_features()
|
avail = self.get_supported_features()
|
||||||
for key in ["udp", "dns", "ipv6"]:
|
for key in ["udp", "dns", "ipv6", "user"]:
|
||||||
if getattr(features, key) and not getattr(avail, key):
|
if getattr(features, key) and not getattr(avail, key):
|
||||||
raise Fatal(
|
raise Fatal(
|
||||||
"Feature %s not supported with method %s.\n" %
|
"Feature %s not supported with method %s.\n" %
|
||||||
(key, self.name))
|
(key, self.name))
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
|
user):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def firewall_command(self, line):
|
@staticmethod
|
||||||
|
def firewall_command(line):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -96,10 +102,14 @@ def get_method(method_name):
|
|||||||
def get_auto_method():
|
def get_auto_method():
|
||||||
if _program_exists('iptables'):
|
if _program_exists('iptables'):
|
||||||
method_name = "nat"
|
method_name = "nat"
|
||||||
|
elif _program_exists('nft'):
|
||||||
|
method_name = "nft"
|
||||||
elif _program_exists('pfctl'):
|
elif _program_exists('pfctl'):
|
||||||
method_name = "pf"
|
method_name = "pf"
|
||||||
|
elif _program_exists('ipfw'):
|
||||||
|
method_name = "ipfw"
|
||||||
else:
|
else:
|
||||||
raise Fatal(
|
raise Fatal(
|
||||||
"can't find either iptables or pfctl; check your PATH")
|
"can't find either iptables, nft or pfctl; check your PATH")
|
||||||
|
|
||||||
return get_method(method_name)
|
return get_method(method_name)
|
||||||
|
263
sshuttle/methods/ipfw.py
Normal file
263
sshuttle/methods/ipfw.py
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import os
|
||||||
|
import subprocess as ssubprocess
|
||||||
|
from sshuttle.methods import BaseMethod
|
||||||
|
from sshuttle.helpers import log, debug1, debug3, \
|
||||||
|
Fatal, family_to_string
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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))
|
||||||
|
dstip = None
|
||||||
|
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||||
|
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
||||||
|
port = 53
|
||||||
|
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
|
||||||
|
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)
|
||||||
|
|
||||||
|
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())
|
||||||
|
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
||||||
|
found = True
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
_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)
|
||||||
|
for line in p.stdout:
|
||||||
|
line = line.decode()
|
||||||
|
assert(line[-1] == '\n')
|
||||||
|
(k, v) = line[:-1].split(': ', 1)
|
||||||
|
_oldctls[k] = v.strip()
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
if not line:
|
||||||
|
raise Fatal('%r returned no data' % (argv,))
|
||||||
|
|
||||||
|
|
||||||
|
def _sysctl_set(name, val):
|
||||||
|
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'))
|
||||||
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
|
|
||||||
|
|
||||||
|
_changedctls = []
|
||||||
|
|
||||||
|
|
||||||
|
def sysctl_set(name, val, permanent=False):
|
||||||
|
PREFIX = 'net.inet.ip'
|
||||||
|
assert(name.startswith(PREFIX + '.'))
|
||||||
|
val = str(val)
|
||||||
|
if not _oldctls:
|
||||||
|
_fill_oldctls(PREFIX)
|
||||||
|
if not (name in _oldctls):
|
||||||
|
debug1('>> No such sysctl: %r\n' % 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')
|
||||||
|
f = open('/etc/sysctl.conf', 'a')
|
||||||
|
f.write('\n'
|
||||||
|
'# Added by sshuttle\n'
|
||||||
|
'%s=%s\n' % (name, val))
|
||||||
|
f.close()
|
||||||
|
else:
|
||||||
|
_changedctls.append(name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ipfw(*args):
|
||||||
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
rv = ssubprocess.call(argv)
|
||||||
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
def ipfw_noexit(*args):
|
||||||
|
argv = ['ipfw', '-q'] + list(args)
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
ssubprocess.call(argv)
|
||||||
|
# No env: No output. (Or error that won't be parsed.)
|
||||||
|
|
||||||
|
|
||||||
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
def get_supported_features(self):
|
||||||
|
result = super(Method, self).get_supported_features()
|
||||||
|
result.ipv6 = False
|
||||||
|
result.udp = False # NOTE: Almost there, kernel patch needed
|
||||||
|
result.dns = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_tcp_dstip(self, sock):
|
||||||
|
return sock.getsockname()
|
||||||
|
|
||||||
|
def recv_udp(self, udp_listener, bufsize):
|
||||||
|
srcip, dstip, data = recv_udp(udp_listener, bufsize)
|
||||||
|
if not dstip:
|
||||||
|
debug1(
|
||||||
|
"-- ignored UDP from %r: "
|
||||||
|
"couldn't determine destination IP address\n" % (srcip,))
|
||||||
|
return None
|
||||||
|
return srcip, dstip, data
|
||||||
|
|
||||||
|
def send_udp(self, sock, srcip, dstip, data):
|
||||||
|
if not srcip:
|
||||||
|
debug1(
|
||||||
|
"-- ignored UDP to %r: "
|
||||||
|
"couldn't determine source IP address\n" % (dstip,))
|
||||||
|
return
|
||||||
|
|
||||||
|
# debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
|
||||||
|
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||||
|
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
||||||
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||||
|
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
|
sender.bind(srcip)
|
||||||
|
sender.sendto(data, dstip)
|
||||||
|
sender.close()
|
||||||
|
|
||||||
|
def setup_udp_listener(self, udp_listener):
|
||||||
|
if udp_listener.v4 is not None:
|
||||||
|
udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1)
|
||||||
|
# if udp_listener.v6 is not None:
|
||||||
|
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
||||||
|
|
||||||
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
|
user):
|
||||||
|
# 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?
|
||||||
|
ipfw_noexit('delete', '1')
|
||||||
|
|
||||||
|
while _changedctls:
|
||||||
|
name = _changedctls.pop()
|
||||||
|
oldval = _oldctls[name]
|
||||||
|
_sysctl_set(name, oldval)
|
||||||
|
|
||||||
|
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', 'skipto', '2',
|
||||||
|
'tcp',
|
||||||
|
'from', 'any', 'to', 'table(125)')
|
||||||
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
|
||||||
|
'tcp',
|
||||||
|
'from', 'any', 'to', 'table(126)',
|
||||||
|
'not', 'ipttl', '42', 'keep-state', 'setup')
|
||||||
|
|
||||||
|
ipfw_noexit('table', '124', 'flush')
|
||||||
|
dnscount = 0
|
||||||
|
for _, ip in [i for i in nslist if i[0] == family]:
|
||||||
|
ipfw('table', '124', 'add', '%s' % (ip))
|
||||||
|
dnscount += 1
|
||||||
|
if dnscount > 0:
|
||||||
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
|
||||||
|
'udp',
|
||||||
|
'from', 'any', 'to', 'table(124)',
|
||||||
|
'not', 'ipttl', '42')
|
||||||
|
ipfw('add', '1', 'allow',
|
||||||
|
'udp',
|
||||||
|
'from', 'any', 'to', 'any',
|
||||||
|
'ipttl', '42')
|
||||||
|
|
||||||
|
if subnets:
|
||||||
|
# create new subnet entries
|
||||||
|
for _, swidth, sexclude, snet in sorted(subnets,
|
||||||
|
key=lambda s: s[1],
|
||||||
|
reverse=True):
|
||||||
|
if sexclude:
|
||||||
|
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
||||||
|
else:
|
||||||
|
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
|
||||||
|
|
||||||
|
def restore_firewall(self, port, family, udp, user):
|
||||||
|
if family not in [socket.AF_INET]:
|
||||||
|
raise Exception(
|
||||||
|
'Address family "%s" unsupported by tproxy method'
|
||||||
|
% family_to_string(family))
|
||||||
|
|
||||||
|
ipfw_noexit('delete', '1')
|
||||||
|
ipfw_noexit('table', '124', 'flush')
|
||||||
|
ipfw_noexit('table', '125', 'flush')
|
||||||
|
ipfw_noexit('table', '126', 'flush')
|
@ -1,4 +1,5 @@
|
|||||||
import socket
|
import socket
|
||||||
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import family_to_string
|
from sshuttle.helpers import family_to_string
|
||||||
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
|
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
@ -11,7 +12,8 @@ class Method(BaseMethod):
|
|||||||
# the multiple copies shouldn't have overlapping subnets, or only the most-
|
# the multiple copies shouldn't have overlapping subnets, or only the most-
|
||||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
# "-A OUTPUT").
|
# "-A OUTPUT").
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
|
user):
|
||||||
# only ipv4 supported with NAT
|
# only ipv4 supported with NAT
|
||||||
if family != socket.AF_INET:
|
if family != socket.AF_INET:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
@ -28,41 +30,62 @@ class Method(BaseMethod):
|
|||||||
def _ipt_ttl(*args):
|
def _ipt_ttl(*args):
|
||||||
return ipt_ttl(family, table, *args)
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
|
def _ipm(*args):
|
||||||
|
return ipt(family, "mangle", *args)
|
||||||
|
|
||||||
chain = 'sshuttle-%s' % port
|
chain = 'sshuttle-%s' % port
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
self.restore_firewall(port, family, udp)
|
self.restore_firewall(port, family, udp, user)
|
||||||
|
|
||||||
_ipt('-N', chain)
|
_ipt('-N', chain)
|
||||||
_ipt('-F', chain)
|
_ipt('-F', chain)
|
||||||
_ipt('-I', 'OUTPUT', '1', '-j', chain)
|
if user is not None:
|
||||||
_ipt('-I', 'PREROUTING', '1', '-j', chain)
|
_ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user),
|
||||||
|
'-j', 'MARK', '--set-mark', str(port))
|
||||||
|
args = '-m', 'mark', '--mark', str(port), '-j', chain
|
||||||
|
else:
|
||||||
|
args = '-j', chain
|
||||||
|
|
||||||
|
_ipt('-I', 'OUTPUT', '1', *args)
|
||||||
|
_ipt('-I', 'PREROUTING', '1', *args)
|
||||||
|
|
||||||
|
# Firstly we always skip all LOCAL addtrype address, i.e. avoid
|
||||||
|
# tunnelling the traffic designated to all local TCP/IP addresses.
|
||||||
|
_ipt('-A', chain, '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype',
|
||||||
|
'--dst-type', 'LOCAL',
|
||||||
|
'!', '-p', 'udp')
|
||||||
|
# Skip LOCAL traffic if it's not DNS.
|
||||||
|
_ipt('-A', chain, '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype',
|
||||||
|
'--dst-type', 'LOCAL',
|
||||||
|
'-p', 'udp', '!', '--dport', '53')
|
||||||
|
|
||||||
|
# create new subnet entries.
|
||||||
|
for _, swidth, sexclude, snet, fport, lport \
|
||||||
|
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||||
|
tcp_ports = ('-p', 'tcp')
|
||||||
|
if fport:
|
||||||
|
tcp_ports = tcp_ports + ('--dport', '%d:%d' % (fport, lport))
|
||||||
|
|
||||||
# create new subnet entries. Note that we're sorting in a very
|
|
||||||
# particular order: we need to go from most-specific (largest
|
|
||||||
# swidth) to least-specific, and at any given level of specificity,
|
|
||||||
# we want excludes to come first. That's why the columns are in
|
|
||||||
# such a non- intuitive order.
|
|
||||||
for f, swidth, sexclude, snet \
|
|
||||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
|
||||||
if sexclude:
|
if sexclude:
|
||||||
_ipt('-A', chain, '-j', 'RETURN',
|
_ipt('-A', chain, '-j', 'RETURN',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-p', 'tcp')
|
*tcp_ports)
|
||||||
else:
|
else:
|
||||||
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-p', 'tcp',
|
*(tcp_ports + ('--to-ports', str(port))))
|
||||||
'--to-ports', str(port))
|
|
||||||
|
|
||||||
for f, ip in [i for i in nslist if i[0] == family]:
|
for _, ip in [i for i in nslist if i[0] == family]:
|
||||||
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
'--dest', '%s/32' % ip,
|
'--dest', '%s/32' % ip,
|
||||||
'-p', 'udp',
|
'-p', 'udp',
|
||||||
'--dport', '53',
|
'--dport', '53',
|
||||||
'--to-ports', str(dnsport))
|
'--to-ports', str(dnsport))
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
# only ipv4 supported with NAT
|
# only ipv4 supported with NAT
|
||||||
if family != socket.AF_INET:
|
if family != socket.AF_INET:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
@ -79,11 +102,25 @@ class Method(BaseMethod):
|
|||||||
def _ipt_ttl(*args):
|
def _ipt_ttl(*args):
|
||||||
return ipt_ttl(family, table, *args)
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
|
def _ipm(*args):
|
||||||
|
return ipt(family, "mangle", *args)
|
||||||
|
|
||||||
chain = 'sshuttle-%s' % port
|
chain = 'sshuttle-%s' % port
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
if ipt_chain_exists(family, table, chain):
|
if ipt_chain_exists(family, table, chain):
|
||||||
nonfatal(_ipt, '-D', 'OUTPUT', '-j', chain)
|
if user is not None:
|
||||||
nonfatal(_ipt, '-D', 'PREROUTING', '-j', chain)
|
nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner',
|
||||||
|
str(user), '-j', 'MARK', '--set-mark', str(port))
|
||||||
|
args = '-m', 'mark', '--mark', str(port), '-j', chain
|
||||||
|
else:
|
||||||
|
args = '-j', chain
|
||||||
|
nonfatal(_ipt, '-D', 'OUTPUT', *args)
|
||||||
|
nonfatal(_ipt, '-D', 'PREROUTING', *args)
|
||||||
nonfatal(_ipt, '-F', chain)
|
nonfatal(_ipt, '-F', chain)
|
||||||
_ipt('-X', chain)
|
_ipt('-X', chain)
|
||||||
|
|
||||||
|
def get_supported_features(self):
|
||||||
|
result = super(Method, self).get_supported_features()
|
||||||
|
result.user = True
|
||||||
|
return result
|
||||||
|
76
sshuttle/methods/nft.py
Normal file
76
sshuttle/methods/nft.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import socket
|
||||||
|
from sshuttle.firewall import subnet_weight
|
||||||
|
from sshuttle.linux import nft, nonfatal
|
||||||
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
# We name the chain based on the transproxy port number so that it's
|
||||||
|
# possible to run multiple copies of sshuttle at the same time. Of course,
|
||||||
|
# the multiple copies shouldn't have overlapping subnets, or only the most-
|
||||||
|
# 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):
|
||||||
|
if udp:
|
||||||
|
raise Exception("UDP not supported by nft")
|
||||||
|
|
||||||
|
table = 'sshuttle-%s' % port
|
||||||
|
|
||||||
|
def _nft(action, *args):
|
||||||
|
return nft(family, table, action, *args)
|
||||||
|
|
||||||
|
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', 'output',
|
||||||
|
'{ type nat hook output priority -100; policy accept; }')
|
||||||
|
_nft('add chain', chain)
|
||||||
|
_nft('flush chain', chain)
|
||||||
|
_nft('add rule', 'output jump %s' % chain)
|
||||||
|
_nft('add rule', 'prerouting jump %s' % chain)
|
||||||
|
|
||||||
|
# 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 and fport != lport:
|
||||||
|
tcp_ports = \
|
||||||
|
tcp_ports + \
|
||||||
|
('tcp', 'dport', '{ %d-%d }' % (fport, lport))
|
||||||
|
elif fport and fport == lport:
|
||||||
|
tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport))
|
||||||
|
|
||||||
|
if sexclude:
|
||||||
|
_nft('add rule', chain, *(tcp_ports + (
|
||||||
|
'ip daddr %s/%s' % (snet, swidth), 'return')))
|
||||||
|
else:
|
||||||
|
_nft('add rule', chain, *(tcp_ports + (
|
||||||
|
'ip daddr %s/%s' % (snet, swidth), 'ip ttl != 42',
|
||||||
|
('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 = 'sshuttle-%s' % port
|
||||||
|
|
||||||
|
def _nft(action, *args):
|
||||||
|
return nft(family, table, action, *args)
|
||||||
|
|
||||||
|
# basic cleanup/setup of chains
|
||||||
|
nonfatal(_nft, 'delete table', '')
|
@ -1,64 +1,328 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
import errno
|
||||||
import struct
|
import struct
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
|
import shlex
|
||||||
from fcntl import ioctl
|
from fcntl import ioctl
|
||||||
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
||||||
sizeof, addressof, memmove
|
sizeof, addressof, memmove
|
||||||
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string
|
from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
def pfctl(args, stdin=None):
|
_pf_context = {
|
||||||
argv = ['pfctl'] + list(args.split(" "))
|
'started_by_sshuttle': 0,
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
'loaded_by_sshuttle': True,
|
||||||
|
'Xtoken': []
|
||||||
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
}
|
||||||
stdout=ssubprocess.PIPE,
|
|
||||||
stderr=ssubprocess.PIPE)
|
|
||||||
o = p.communicate(stdin)
|
|
||||||
if p.returncode:
|
|
||||||
raise Fatal('%r returned %d' % (argv, p.returncode))
|
|
||||||
|
|
||||||
return o
|
|
||||||
|
|
||||||
_pf_context = {'started_by_sshuttle': False, 'Xtoken': None}
|
|
||||||
_pf_fd = None
|
_pf_fd = None
|
||||||
|
|
||||||
|
|
||||||
class OsDefs(object):
|
class Generic(object):
|
||||||
|
MAXPATHLEN = 1024
|
||||||
|
PF_CHANGE_ADD_TAIL = 2
|
||||||
|
PF_CHANGE_GET_TICKET = 6
|
||||||
|
PF_PASS = 0
|
||||||
|
PF_RDR = 8
|
||||||
|
PF_OUT = 2
|
||||||
|
ACTION_OFFSET = 0
|
||||||
|
POOL_TICKET_OFFSET = 8
|
||||||
|
ANCHOR_CALL_OFFSET = 1040
|
||||||
|
|
||||||
def __init__(self, platform=None):
|
class pf_addr(Structure):
|
||||||
if platform is None:
|
class _pfa(Union):
|
||||||
platform = sys.platform
|
_fields_ = [("v4", c_uint32), # struct in_addr
|
||||||
self.platform = platform
|
("v6", c_uint32 * 4), # struct in6_addr
|
||||||
|
("addr8", c_uint8 * 16),
|
||||||
|
("addr16", c_uint16 * 8),
|
||||||
|
("addr32", c_uint32 * 4)]
|
||||||
|
|
||||||
# This are some classes and functions used to support pf in yosemite.
|
_fields_ = [("pfa", _pfa)]
|
||||||
if platform == 'darwin':
|
_anonymous_ = ("pfa",)
|
||||||
class pf_state_xport(Union):
|
|
||||||
_fields_ = [("port", c_uint16),
|
|
||||||
("call_id", c_uint16),
|
|
||||||
("spi", c_uint32)]
|
|
||||||
else:
|
|
||||||
class pf_state_xport(Union):
|
|
||||||
_fields_ = [("port", c_uint16),
|
|
||||||
("call_id", c_uint16)]
|
|
||||||
|
|
||||||
class pf_addr(Structure):
|
def __init__(self):
|
||||||
|
self.status = b''
|
||||||
|
self.pfioc_pooladdr = c_char * 1136
|
||||||
|
|
||||||
class _pfa(Union):
|
self.DIOCNATLOOK = (
|
||||||
_fields_ = [("v4", c_uint32), # struct in_addr
|
(0x40000000 | 0x80000000) |
|
||||||
("v6", c_uint32 * 4), # struct in6_addr
|
((sizeof(self.pfioc_natlook) & 0x1fff) << 16) |
|
||||||
("addr8", c_uint8 * 16),
|
((ord('D')) << 8) | (23))
|
||||||
("addr16", c_uint16 * 8),
|
self.DIOCCHANGERULE = (
|
||||||
("addr32", c_uint32 * 4)]
|
(0x40000000 | 0x80000000) |
|
||||||
|
((sizeof(self.pfioc_rule) & 0x1fff) << 16) |
|
||||||
|
((ord('D')) << 8) | (26))
|
||||||
|
self.DIOCBEGINADDRS = (
|
||||||
|
(0x40000000 | 0x80000000) |
|
||||||
|
((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) |
|
||||||
|
((ord('D')) << 8) | (51))
|
||||||
|
|
||||||
_fields_ = [("pfa", _pfa)]
|
def enable(self):
|
||||||
_anonymous_ = ("pfa",)
|
if b'INFO:\nStatus: Disabled' in self.status:
|
||||||
|
pfctl('-e')
|
||||||
|
_pf_context['started_by_sshuttle'] += 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def disable(anchor):
|
||||||
|
pfctl('-a %s -F all' % anchor)
|
||||||
|
if _pf_context['started_by_sshuttle'] == 1:
|
||||||
|
pfctl('-d')
|
||||||
|
_pf_context['started_by_sshuttle'] -= 1
|
||||||
|
|
||||||
|
def query_nat(self, family, proto, src_ip, src_port, dst_ip, dst_port):
|
||||||
|
[proto, family, src_port, dst_port] = [
|
||||||
|
int(v) for v in [proto, family, src_port, dst_port]]
|
||||||
|
|
||||||
|
packed_src_ip = socket.inet_pton(family, src_ip)
|
||||||
|
packed_dst_ip = socket.inet_pton(family, dst_ip)
|
||||||
|
|
||||||
|
assert len(packed_src_ip) == len(packed_dst_ip)
|
||||||
|
length = len(packed_src_ip)
|
||||||
|
|
||||||
|
pnl = self.pfioc_natlook()
|
||||||
|
pnl.proto = proto
|
||||||
|
pnl.direction = self.PF_OUT
|
||||||
|
pnl.af = family
|
||||||
|
memmove(addressof(pnl.saddr), packed_src_ip, length)
|
||||||
|
memmove(addressof(pnl.daddr), packed_dst_ip, length)
|
||||||
|
self._add_natlook_ports(pnl, src_port, dst_port)
|
||||||
|
|
||||||
|
ioctl(pf_get_dev(), self.DIOCNATLOOK,
|
||||||
|
(c_char * sizeof(pnl)).from_address(addressof(pnl)))
|
||||||
|
|
||||||
|
ip = socket.inet_ntop(
|
||||||
|
pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw)
|
||||||
|
port = socket.ntohs(self._get_natlook_port(pnl.rdxport))
|
||||||
|
return (ip, port)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_natlook_ports(pnl, src_port, dst_port):
|
||||||
|
pnl.sxport = socket.htons(src_port)
|
||||||
|
pnl.dxport = socket.htons(dst_port)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_natlook_port(xport):
|
||||||
|
return xport
|
||||||
|
|
||||||
|
def add_anchors(self, anchor, status=None):
|
||||||
|
if status is None:
|
||||||
|
status = pfctl('-s all')[0]
|
||||||
|
self.status = status
|
||||||
|
if ('\nanchor "%s"' % anchor).encode('ASCII') not in status:
|
||||||
|
self._add_anchor_rule(self.PF_PASS, anchor.encode('ASCII'))
|
||||||
|
|
||||||
|
def _add_anchor_rule(self, kind, name, pr=None):
|
||||||
|
if pr is None:
|
||||||
|
pr = self.pfioc_rule()
|
||||||
|
|
||||||
|
memmove(addressof(pr) + self.ANCHOR_CALL_OFFSET, name,
|
||||||
|
min(self.MAXPATHLEN, len(name))) # anchor_call = name
|
||||||
|
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
|
||||||
|
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
|
||||||
|
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _inet_version(family):
|
||||||
|
return b'inet' if family == socket.AF_INET else b'inet6'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _lo_addr(family):
|
||||||
|
return b'127.0.0.1' if family == socket.AF_INET else b'::1'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_rules(anchor, rules):
|
||||||
|
assert isinstance(rules, bytes)
|
||||||
|
debug3("rules:\n" + rules.decode("ASCII"))
|
||||||
|
pfctl('-a %s -f /dev/stdin' % anchor, rules)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def has_skip_loopback():
|
||||||
|
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]
|
||||||
|
|
||||||
|
|
||||||
|
class FreeBsd(Generic):
|
||||||
|
RULE_ACTION_OFFSET = 2968
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
class pfioc_natlook(Structure):
|
||||||
|
pf_addr = Generic.pf_addr
|
||||||
|
_fields_ = [("saddr", pf_addr),
|
||||||
|
("daddr", pf_addr),
|
||||||
|
("rsaddr", pf_addr),
|
||||||
|
("rdaddr", pf_addr),
|
||||||
|
("sxport", c_uint16),
|
||||||
|
("dxport", c_uint16),
|
||||||
|
("rsxport", c_uint16),
|
||||||
|
("rdxport", c_uint16),
|
||||||
|
("af", c_uint8), # sa_family_t
|
||||||
|
("proto", c_uint8),
|
||||||
|
("proto_variant", c_uint8),
|
||||||
|
("direction", c_uint8)]
|
||||||
|
|
||||||
|
freebsd = Generic.__new__(cls)
|
||||||
|
freebsd.pfioc_rule = c_char * 3040
|
||||||
|
freebsd.pfioc_natlook = pfioc_natlook
|
||||||
|
return freebsd
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
returncode = ssubprocess.call(['kldload', 'pf'])
|
||||||
|
# No env: No output.
|
||||||
|
super(FreeBsd, self).enable()
|
||||||
|
if returncode == 0:
|
||||||
|
_pf_context['loaded_by_sshuttle'] = True
|
||||||
|
|
||||||
|
def disable(self, anchor):
|
||||||
|
super(FreeBsd, self).disable(anchor)
|
||||||
|
if _pf_context['loaded_by_sshuttle'] and \
|
||||||
|
_pf_context['started_by_sshuttle'] == 0:
|
||||||
|
ssubprocess.call(['kldunload', 'pf'])
|
||||||
|
# No env: No output.
|
||||||
|
|
||||||
|
def add_anchors(self, anchor):
|
||||||
|
status = pfctl('-s all')[0]
|
||||||
|
if ('\nrdr-anchor "%s"' % anchor).encode('ASCII') not in status:
|
||||||
|
self._add_anchor_rule(self.PF_RDR, anchor.encode('ASCII'))
|
||||||
|
super(FreeBsd, self).add_anchors(anchor, status=status)
|
||||||
|
|
||||||
|
def _add_anchor_rule(self, kind, name, pr=None):
|
||||||
|
pr = pr or self.pfioc_rule()
|
||||||
|
ppa = self.pfioc_pooladdr()
|
||||||
|
|
||||||
|
ioctl(pf_get_dev(), self.DIOCBEGINADDRS, ppa)
|
||||||
|
# pool ticket
|
||||||
|
memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4)
|
||||||
|
super(FreeBsd, self)._add_anchor_rule(kind, name, pr=pr)
|
||||||
|
|
||||||
|
def add_rules(self, anchor, includes, port, dnsport, nslist, family):
|
||||||
|
inet_version = self._inet_version(family)
|
||||||
|
lo_addr = self._lo_addr(family)
|
||||||
|
|
||||||
|
tables = []
|
||||||
|
translating_rules = [
|
||||||
|
b'rdr pass on lo0 %s proto tcp from ! %s to %s '
|
||||||
|
b'-> %s port %r' % (inet_version, lo_addr, subnet, lo_addr, port)
|
||||||
|
for exclude, subnet in includes if not exclude
|
||||||
|
]
|
||||||
|
filtering_rules = [
|
||||||
|
b'pass out route-to lo0 %s proto tcp '
|
||||||
|
b'to %s keep state' % (inet_version, subnet)
|
||||||
|
if not exclude else
|
||||||
|
b'pass out %s proto tcp to %s' % (inet_version, subnet)
|
||||||
|
for exclude, subnet in includes
|
||||||
|
]
|
||||||
|
|
||||||
|
if nslist:
|
||||||
|
tables.append(
|
||||||
|
b'table <dns_servers> {%s}' %
|
||||||
|
b','.join([ns[1].encode("ASCII") for ns in nslist]))
|
||||||
|
translating_rules.append(
|
||||||
|
b'rdr pass on lo0 %s proto udp to <dns_servers> '
|
||||||
|
b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport))
|
||||||
|
filtering_rules.append(
|
||||||
|
b'pass out route-to lo0 %s proto udp to '
|
||||||
|
b'<dns_servers> port 53 keep state' % inet_version)
|
||||||
|
|
||||||
|
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
|
||||||
|
+ b'\n'
|
||||||
|
|
||||||
|
super(FreeBsd, self).add_rules(anchor, rules)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenBsd(Generic):
|
||||||
|
POOL_TICKET_OFFSET = 4
|
||||||
|
RULE_ACTION_OFFSET = 3324
|
||||||
|
ANCHOR_CALL_OFFSET = 1036
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
class pfioc_natlook(Structure):
|
||||||
|
pf_addr = Generic.pf_addr
|
||||||
|
_fields_ = [("saddr", pf_addr),
|
||||||
|
("daddr", pf_addr),
|
||||||
|
("rsaddr", pf_addr),
|
||||||
|
("rdaddr", pf_addr),
|
||||||
|
("rdomain", c_uint16),
|
||||||
|
("rrdomain", c_uint16),
|
||||||
|
("sxport", c_uint16),
|
||||||
|
("dxport", c_uint16),
|
||||||
|
("rsxport", c_uint16),
|
||||||
|
("rdxport", c_uint16),
|
||||||
|
("af", c_uint8), # sa_family_t
|
||||||
|
("proto", c_uint8),
|
||||||
|
("proto_variant", c_uint8),
|
||||||
|
("direction", c_uint8)]
|
||||||
|
|
||||||
|
self.pfioc_rule = c_char * 3424
|
||||||
|
self.pfioc_natlook = pfioc_natlook
|
||||||
|
super(OpenBsd, self).__init__()
|
||||||
|
|
||||||
|
def add_anchors(self, anchor):
|
||||||
|
# before adding anchors and rules we must override the skip lo
|
||||||
|
# that comes by default in openbsd pf.conf so the rules we will add,
|
||||||
|
# which rely on translating/filtering packets on lo, can work
|
||||||
|
if self.has_skip_loopback():
|
||||||
|
pfctl('-f /dev/stdin', b'match on lo\n')
|
||||||
|
super(OpenBsd, self).add_anchors(anchor)
|
||||||
|
|
||||||
|
def add_rules(self, anchor, includes, port, dnsport, nslist, family):
|
||||||
|
inet_version = self._inet_version(family)
|
||||||
|
lo_addr = self._lo_addr(family)
|
||||||
|
|
||||||
|
tables = []
|
||||||
|
translating_rules = [
|
||||||
|
b'pass in on lo0 %s proto tcp to %s '
|
||||||
|
b'divert-to %s port %r' % (inet_version, subnet, lo_addr, port)
|
||||||
|
for exclude, subnet in includes if not exclude
|
||||||
|
]
|
||||||
|
filtering_rules = [
|
||||||
|
b'pass out %s proto tcp to %s '
|
||||||
|
b'route-to lo0 keep state' % (inet_version, subnet)
|
||||||
|
if not exclude else
|
||||||
|
b'pass out %s proto tcp to %s' % (inet_version, subnet)
|
||||||
|
for exclude, subnet in includes
|
||||||
|
]
|
||||||
|
|
||||||
|
if nslist:
|
||||||
|
tables.append(
|
||||||
|
b'table <dns_servers> {%s}' %
|
||||||
|
b','.join([ns[1].encode("ASCII") for ns in nslist]))
|
||||||
|
translating_rules.append(
|
||||||
|
b'pass in on lo0 %s proto udp to <dns_servers> port 53 '
|
||||||
|
b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport))
|
||||||
|
filtering_rules.append(
|
||||||
|
b'pass out %s proto udp to <dns_servers> port 53 '
|
||||||
|
b'route-to lo0 keep state' % inet_version)
|
||||||
|
|
||||||
|
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
|
||||||
|
+ b'\n'
|
||||||
|
|
||||||
|
super(OpenBsd, self).add_rules(anchor, rules)
|
||||||
|
|
||||||
|
|
||||||
|
class Darwin(FreeBsd):
|
||||||
|
RULE_ACTION_OFFSET = 3068
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
class pf_state_xport(Union):
|
||||||
|
_fields_ = [("port", c_uint16),
|
||||||
|
("call_id", c_uint16),
|
||||||
|
("spi", c_uint32)]
|
||||||
|
|
||||||
class pfioc_natlook(Structure):
|
class pfioc_natlook(Structure):
|
||||||
|
pf_addr = Generic.pf_addr
|
||||||
_fields_ = [("saddr", pf_addr),
|
_fields_ = [("saddr", pf_addr),
|
||||||
("daddr", pf_addr),
|
("daddr", pf_addr),
|
||||||
("rsaddr", pf_addr),
|
("rsaddr", pf_addr),
|
||||||
@ -71,39 +335,71 @@ class OsDefs(object):
|
|||||||
("proto", c_uint8),
|
("proto", c_uint8),
|
||||||
("proto_variant", c_uint8),
|
("proto_variant", c_uint8),
|
||||||
("direction", c_uint8)]
|
("direction", c_uint8)]
|
||||||
|
|
||||||
|
self.pfioc_rule = c_char * 3104
|
||||||
self.pfioc_natlook = pfioc_natlook
|
self.pfioc_natlook = pfioc_natlook
|
||||||
|
super(Darwin, self).__init__()
|
||||||
|
|
||||||
# sizeof(struct pfioc_rule)
|
def enable(self):
|
||||||
self.pfioc_rule = c_char * \
|
o = pfctl('-E')
|
||||||
(3104 if platform == 'darwin' else 3040)
|
_pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1))
|
||||||
|
|
||||||
# sizeof(struct pfioc_pooladdr)
|
def disable(self, anchor):
|
||||||
self.pfioc_pooladdr = c_char * 1136
|
pfctl('-a %s -F all' % anchor)
|
||||||
|
if _pf_context['Xtoken']:
|
||||||
|
pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII"))
|
||||||
|
|
||||||
self.MAXPATHLEN = 1024
|
def add_anchors(self, anchor):
|
||||||
|
# before adding anchors and rules we must override the skip lo
|
||||||
|
# that in some cases ends up in the chain so the rules we will add,
|
||||||
|
# which rely on translating/filtering packets on lo, can work
|
||||||
|
if self.has_skip_loopback():
|
||||||
|
pfctl('-f /dev/stdin', b'pass on lo\n')
|
||||||
|
super(Darwin, self).add_anchors(anchor)
|
||||||
|
|
||||||
self.DIOCNATLOOK = (
|
def _add_natlook_ports(self, pnl, src_port, dst_port):
|
||||||
(0x40000000 | 0x80000000) |
|
pnl.sxport.port = socket.htons(src_port)
|
||||||
((sizeof(pfioc_natlook) & 0x1fff) << 16) |
|
pnl.dxport.port = socket.htons(dst_port)
|
||||||
((ord('D')) << 8) | (23))
|
|
||||||
self.DIOCCHANGERULE = (
|
|
||||||
(0x40000000 | 0x80000000) |
|
|
||||||
((sizeof(self.pfioc_rule) & 0x1fff) << 16) |
|
|
||||||
((ord('D')) << 8) | (26))
|
|
||||||
self.DIOCBEGINADDRS = (
|
|
||||||
(0x40000000 | 0x80000000) |
|
|
||||||
((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) |
|
|
||||||
((ord('D')) << 8) | (51))
|
|
||||||
|
|
||||||
self.PF_CHANGE_ADD_TAIL = 2
|
def _get_natlook_port(self, xport):
|
||||||
self.PF_CHANGE_GET_TICKET = 6
|
return xport.port
|
||||||
|
|
||||||
self.PF_PASS = 0
|
|
||||||
self.PF_RDR = 8
|
|
||||||
|
|
||||||
self.PF_OUT = 2
|
class PfSense(FreeBsd):
|
||||||
|
RULE_ACTION_OFFSET = 3040
|
||||||
|
|
||||||
osdefs = OsDefs()
|
def __init__(self):
|
||||||
|
self.pfioc_rule = c_char * 3112
|
||||||
|
super(PfSense, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
pf = Darwin()
|
||||||
|
elif sys.platform.startswith('openbsd'):
|
||||||
|
pf = OpenBsd()
|
||||||
|
elif platform.version().endswith('pfSense'):
|
||||||
|
pf = PfSense()
|
||||||
|
else:
|
||||||
|
pf = FreeBsd()
|
||||||
|
|
||||||
|
|
||||||
|
def pfctl(args, stdin=None):
|
||||||
|
argv = ['pfctl'] + shlex.split(args)
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
||||||
|
stdout=ssubprocess.PIPE,
|
||||||
|
stderr=ssubprocess.PIPE,
|
||||||
|
env=env)
|
||||||
|
o = p.communicate(stdin)
|
||||||
|
if p.returncode:
|
||||||
|
raise Fatal('%r returned %d' % (argv, p.returncode))
|
||||||
|
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
def pf_get_dev():
|
def pf_get_dev():
|
||||||
@ -114,66 +410,27 @@ def pf_get_dev():
|
|||||||
return _pf_fd
|
return _pf_fd
|
||||||
|
|
||||||
|
|
||||||
def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
|
def pf_get_anchor(family, port):
|
||||||
[proto, family, src_port, dst_port] = [
|
return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port)
|
||||||
int(v) for v in [proto, family, src_port, dst_port]]
|
|
||||||
|
|
||||||
packed_src_ip = socket.inet_pton(family, src_ip)
|
|
||||||
packed_dst_ip = socket.inet_pton(family, dst_ip)
|
|
||||||
|
|
||||||
assert len(packed_src_ip) == len(packed_dst_ip)
|
|
||||||
length = len(packed_src_ip)
|
|
||||||
|
|
||||||
pnl = osdefs.pfioc_natlook()
|
|
||||||
pnl.proto = proto
|
|
||||||
pnl.direction = osdefs.PF_OUT
|
|
||||||
pnl.af = family
|
|
||||||
memmove(addressof(pnl.saddr), packed_src_ip, length)
|
|
||||||
memmove(addressof(pnl.daddr), packed_dst_ip, length)
|
|
||||||
pnl.sxport.port = socket.htons(src_port)
|
|
||||||
pnl.dxport.port = socket.htons(dst_port)
|
|
||||||
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCNATLOOK,
|
|
||||||
(c_char * sizeof(pnl)).from_address(addressof(pnl)))
|
|
||||||
|
|
||||||
ip = socket.inet_ntop(
|
|
||||||
pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw)
|
|
||||||
port = socket.ntohs(pnl.rdxport.port)
|
|
||||||
return (ip, port)
|
|
||||||
|
|
||||||
|
|
||||||
def pf_add_anchor_rule(type, name):
|
|
||||||
ACTION_OFFSET = 0
|
|
||||||
POOL_TICKET_OFFSET = 8
|
|
||||||
ANCHOR_CALL_OFFSET = 1040
|
|
||||||
RULE_ACTION_OFFSET = 3068 if osdefs.platform == 'darwin' else 2968
|
|
||||||
|
|
||||||
pr = osdefs.pfioc_rule()
|
|
||||||
ppa = osdefs.pfioc_pooladdr()
|
|
||||||
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCBEGINADDRS, ppa)
|
|
||||||
|
|
||||||
memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) # pool_ticket
|
|
||||||
memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name,
|
|
||||||
min(osdefs.MAXPATHLEN, len(name))) # anchor_call = name
|
|
||||||
memmove(addressof(pr) + RULE_ACTION_OFFSET,
|
|
||||||
struct.pack('I', type), 4) # rule.action = type
|
|
||||||
|
|
||||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
|
|
||||||
'I', osdefs.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
|
|
||||||
|
|
||||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
|
|
||||||
'I', osdefs.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
|
|
||||||
ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
|
|
||||||
|
|
||||||
|
|
||||||
class Method(BaseMethod):
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
def get_supported_features(self):
|
||||||
|
result = super(Method, self).get_supported_features()
|
||||||
|
result.ipv6 = True
|
||||||
|
return result
|
||||||
|
|
||||||
def get_tcp_dstip(self, sock):
|
def get_tcp_dstip(self, sock):
|
||||||
pfile = self.firewall.pfile
|
pfile = self.firewall.pfile
|
||||||
|
|
||||||
peer = sock.getpeername()
|
try:
|
||||||
|
peer = sock.getpeername()
|
||||||
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
|
if e.args[0] == errno.EINVAL:
|
||||||
|
return sock.getsockname()
|
||||||
|
|
||||||
proxy = sock.getsockname()
|
proxy = sock.getsockname()
|
||||||
|
|
||||||
argv = (sock.family, socket.IPPROTO_TCP,
|
argv = (sock.family, socket.IPPROTO_TCP,
|
||||||
@ -190,89 +447,46 @@ class Method(BaseMethod):
|
|||||||
|
|
||||||
return sock.getsockname()
|
return sock.getsockname()
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
tables = []
|
user):
|
||||||
translating_rules = []
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
filtering_rules = []
|
|
||||||
|
|
||||||
if family != socket.AF_INET:
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by pf method_name'
|
'Address family "%s" unsupported by pf method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by pf method_name")
|
raise Exception("UDP not supported by pf method_name")
|
||||||
|
|
||||||
if len(subnets) > 0:
|
if subnets:
|
||||||
includes = []
|
includes = []
|
||||||
# If a given subnet is both included and excluded, list the
|
# If a given subnet is both included and excluded, list the
|
||||||
# exclusion first; the table will ignore the second, opposite
|
# exclusion first; the table will ignore the second, opposite
|
||||||
# definition
|
# definition
|
||||||
for f, swidth, sexclude, snet in sorted(
|
for _, swidth, sexclude, snet, fport, lport \
|
||||||
subnets, key=lambda s: (s[1], s[2]), reverse=True):
|
in sorted(subnets, key=subnet_weight):
|
||||||
includes.append(b"%s%s/%d" %
|
includes.append((sexclude, b"%s/%d%s" % (
|
||||||
(b"!" if sexclude else b"",
|
snet.encode("ASCII"),
|
||||||
snet.encode("ASCII"),
|
swidth,
|
||||||
swidth))
|
b" port %d:%d" % (fport, lport) if fport else b"")))
|
||||||
|
|
||||||
tables.append(
|
anchor = pf_get_anchor(family, port)
|
||||||
b'table <forward_subnets> {%s}' % b','.join(includes))
|
pf.add_anchors(anchor)
|
||||||
translating_rules.append(
|
pf.add_rules(anchor, includes, port, dnsport, nslist, family)
|
||||||
b'rdr pass on lo0 proto tcp '
|
pf.enable()
|
||||||
b'to <forward_subnets> -> 127.0.0.1 port %r' % port)
|
|
||||||
filtering_rules.append(
|
|
||||||
b'pass out route-to lo0 inet proto tcp '
|
|
||||||
b'to <forward_subnets> keep state')
|
|
||||||
|
|
||||||
if len(nslist) > 0:
|
def restore_firewall(self, port, family, udp, user):
|
||||||
tables.append(
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
b'table <dns_servers> {%s}' %
|
|
||||||
b','.join([ns[1].encode("ASCII") for ns in nslist]))
|
|
||||||
translating_rules.append(
|
|
||||||
b'rdr pass on lo0 proto udp to '
|
|
||||||
b'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
|
|
||||||
filtering_rules.append(
|
|
||||||
b'pass out route-to lo0 inet proto udp to '
|
|
||||||
b'<dns_servers> port 53 keep state')
|
|
||||||
|
|
||||||
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
|
|
||||||
+ b'\n'
|
|
||||||
assert isinstance(rules, bytes)
|
|
||||||
debug3("rules:\n" + rules.decode("ASCII"))
|
|
||||||
|
|
||||||
pf_status = pfctl('-s all')[0]
|
|
||||||
if b'\nrdr-anchor "sshuttle" all\n' not in pf_status:
|
|
||||||
pf_add_anchor_rule(osdefs.PF_RDR, b"sshuttle")
|
|
||||||
if b'\nanchor "sshuttle" all\n' not in pf_status:
|
|
||||||
pf_add_anchor_rule(osdefs.PF_PASS, b"sshuttle")
|
|
||||||
|
|
||||||
pfctl('-a sshuttle -f /dev/stdin', rules)
|
|
||||||
if osdefs.platform == "darwin":
|
|
||||||
o = pfctl('-E')
|
|
||||||
_pf_context['Xtoken'] = \
|
|
||||||
re.search(b'Token : (.+)', o[1]).group(1)
|
|
||||||
elif b'INFO:\nStatus: Disabled' in pf_status:
|
|
||||||
pfctl('-e')
|
|
||||||
_pf_context['started_by_sshuttle'] = True
|
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp):
|
|
||||||
if family != socket.AF_INET:
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by pf method_name'
|
'Address family "%s" unsupported by pf method_name'
|
||||||
% family_to_string(family))
|
% family_to_string(family))
|
||||||
if udp:
|
if udp:
|
||||||
raise Exception("UDP not supported by pf method_name")
|
raise Exception("UDP not supported by pf method_name")
|
||||||
|
|
||||||
pfctl('-a sshuttle -F all')
|
pf.disable(pf_get_anchor(family, port))
|
||||||
if osdefs.platform == "darwin":
|
|
||||||
if _pf_context['Xtoken'] is not None:
|
|
||||||
pfctl('-X %s' % _pf_context['Xtoken'].decode("ASCII"))
|
|
||||||
elif _pf_context['started_by_sshuttle']:
|
|
||||||
pfctl('-d')
|
|
||||||
|
|
||||||
def firewall_command(self, line):
|
def firewall_command(self, line):
|
||||||
if line.startswith('QUERY_PF_NAT '):
|
if line.startswith('QUERY_PF_NAT '):
|
||||||
try:
|
try:
|
||||||
dst = pf_query_nat(*(line[13:].split(',')))
|
dst = pf.query_nat(*(line[13:].split(',')))
|
||||||
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
|
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
|
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import struct
|
import struct
|
||||||
|
from sshuttle.firewall import subnet_weight
|
||||||
from sshuttle.helpers import family_to_string
|
from sshuttle.helpers import family_to_string
|
||||||
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
|
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
|
||||||
from sshuttle.methods import BaseMethod
|
from sshuttle.methods import BaseMethod
|
||||||
@ -32,7 +33,7 @@ IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
|||||||
if recvmsg == "python":
|
if recvmsg == "python":
|
||||||
def recv_udp(listener, bufsize):
|
def recv_udp(listener, bufsize):
|
||||||
debug3('Accept UDP python using recvmsg.\n')
|
debug3('Accept UDP python using recvmsg.\n')
|
||||||
data, ancdata, msg_flags, srcip = listener.recvmsg(
|
data, ancdata, _, srcip = listener.recvmsg(
|
||||||
4096, socket.CMSG_SPACE(24))
|
4096, socket.CMSG_SPACE(24))
|
||||||
dstip = None
|
dstip = None
|
||||||
family = None
|
family = None
|
||||||
@ -59,12 +60,11 @@ if recvmsg == "python":
|
|||||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||||
dstip = (ip, port)
|
dstip = (ip, port)
|
||||||
break
|
break
|
||||||
print("xxxxx", srcip, dstip)
|
|
||||||
return (srcip, dstip, data)
|
return (srcip, dstip, data)
|
||||||
elif recvmsg == "socket_ext":
|
elif recvmsg == "socket_ext":
|
||||||
def recv_udp(listener, bufsize):
|
def recv_udp(listener, bufsize):
|
||||||
debug3('Accept UDP using socket_ext recvmsg.\n')
|
debug3('Accept UDP using socket_ext recvmsg.\n')
|
||||||
srcip, data, adata, flags = listener.recvmsg(
|
srcip, data, adata, _ = listener.recvmsg(
|
||||||
(bufsize,), socket.CMSG_SPACE(24))
|
(bufsize,), socket.CMSG_SPACE(24))
|
||||||
dstip = None
|
dstip = None
|
||||||
family = None
|
family = None
|
||||||
@ -150,7 +150,8 @@ class Method(BaseMethod):
|
|||||||
if udp_listener.v6 is not None:
|
if udp_listener.v6 is not None:
|
||||||
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
||||||
|
|
||||||
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
||||||
|
user):
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by tproxy method'
|
'Address family "%s" unsupported by tproxy method'
|
||||||
@ -164,12 +165,16 @@ class Method(BaseMethod):
|
|||||||
def _ipt_ttl(*args):
|
def _ipt_ttl(*args):
|
||||||
return ipt_ttl(family, table, *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
|
mark_chain = 'sshuttle-m-%s' % port
|
||||||
tproxy_chain = 'sshuttle-t-%s' % port
|
tproxy_chain = 'sshuttle-t-%s' % port
|
||||||
divert_chain = 'sshuttle-d-%s' % port
|
divert_chain = 'sshuttle-d-%s' % port
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
self.restore_firewall(port, family, udp)
|
self.restore_firewall(port, family, udp, user)
|
||||||
|
|
||||||
_ipt('-N', mark_chain)
|
_ipt('-N', mark_chain)
|
||||||
_ipt('-F', mark_chain)
|
_ipt('-F', mark_chain)
|
||||||
@ -188,7 +193,7 @@ class Method(BaseMethod):
|
|||||||
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||||
'-m', 'udp', '-p', 'udp')
|
'-m', 'udp', '-p', 'udp')
|
||||||
|
|
||||||
for f, ip in [i for i in nslist if i[0] == family]:
|
for _, ip in [i for i in nslist if i[0] == family]:
|
||||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
'--dest', '%s/32' % ip,
|
'--dest', '%s/32' % ip,
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
||||||
@ -198,44 +203,56 @@ class Method(BaseMethod):
|
|||||||
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
||||||
'--on-port', str(dnsport))
|
'--on-port', str(dnsport))
|
||||||
|
|
||||||
for f, swidth, sexclude, snet \
|
for _, swidth, sexclude, snet, fport, lport \
|
||||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
in sorted(subnets, key=subnet_weight, reverse=True):
|
||||||
|
tcp_ports = ('-p', 'tcp')
|
||||||
|
tcp_ports = _ipt_proto_ports(tcp_ports, fport, lport)
|
||||||
|
|
||||||
if sexclude:
|
if sexclude:
|
||||||
_ipt('-A', mark_chain, '-j', 'RETURN',
|
_ipt('-A', mark_chain, '-j', 'RETURN',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
'-m', 'tcp',
|
||||||
|
*tcp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'RETURN',
|
_ipt('-A', tproxy_chain, '-j', 'RETURN',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
'-m', 'tcp',
|
||||||
|
*tcp_ports)
|
||||||
else:
|
else:
|
||||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
'-m', 'tcp',
|
||||||
|
*tcp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1',
|
'--tproxy-mark', '0x1/0x1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'tcp', '-p', 'tcp',
|
'-m', 'tcp',
|
||||||
'--on-port', str(port))
|
*(tcp_ports + ('--on-port', str(port))))
|
||||||
|
|
||||||
if udp:
|
if udp:
|
||||||
|
udp_ports = ('-p', 'udp')
|
||||||
|
udp_ports = _ipt_proto_ports(udp_ports, fport, lport)
|
||||||
|
|
||||||
if sexclude:
|
if sexclude:
|
||||||
_ipt('-A', mark_chain, '-j', 'RETURN',
|
_ipt('-A', mark_chain, '-j', 'RETURN',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'udp', '-p', 'udp')
|
'-m', 'udp',
|
||||||
|
*udp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'RETURN',
|
_ipt('-A', tproxy_chain, '-j', 'RETURN',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'udp', '-p', 'udp')
|
'-m', 'udp',
|
||||||
|
*udp_ports)
|
||||||
else:
|
else:
|
||||||
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'udp', '-p', 'udp')
|
'-m', 'udp',
|
||||||
|
*udp_ports)
|
||||||
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
'--tproxy-mark', '0x1/0x1',
|
'--tproxy-mark', '0x1/0x1',
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
'-m', 'udp', '-p', 'udp',
|
'-m', 'udp',
|
||||||
'--on-port', str(port))
|
*(udp_ports + ('--on-port', str(port))))
|
||||||
|
|
||||||
def restore_firewall(self, port, family, udp):
|
def restore_firewall(self, port, family, udp, user):
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Address family "%s" unsupported by tproxy method'
|
'Address family "%s" unsupported by tproxy method'
|
||||||
|
@ -1,215 +1,362 @@
|
|||||||
"""Command-line options parser.
|
|
||||||
With the help of an options spec string, easily parse command-line options.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import textwrap
|
|
||||||
import getopt
|
|
||||||
import re
|
import re
|
||||||
import struct
|
import socket
|
||||||
|
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
|
||||||
|
|
||||||
|
from sshuttle import __version__
|
||||||
|
|
||||||
|
|
||||||
class OptDict:
|
# Subnet file, supporting empty lines and hash-started comment lines
|
||||||
|
def parse_subnetport_file(s):
|
||||||
def __init__(self):
|
|
||||||
self._opts = {}
|
|
||||||
|
|
||||||
def __setitem__(self, k, v):
|
|
||||||
if k.startswith('no-') or k.startswith('no_'):
|
|
||||||
k = k[3:]
|
|
||||||
v = not v
|
|
||||||
self._opts[k] = v
|
|
||||||
|
|
||||||
def __getitem__(self, k):
|
|
||||||
if k.startswith('no-') or k.startswith('no_'):
|
|
||||||
return not self._opts[k[3:]]
|
|
||||||
return self._opts[k]
|
|
||||||
|
|
||||||
def __getattr__(self, k):
|
|
||||||
return self[k]
|
|
||||||
|
|
||||||
|
|
||||||
def _default_onabort(msg):
|
|
||||||
sys.exit(97)
|
|
||||||
|
|
||||||
|
|
||||||
def _intify(v):
|
|
||||||
try:
|
try:
|
||||||
vv = int(v or '')
|
handle = open(s, 'r')
|
||||||
if str(vv) == v:
|
except OSError:
|
||||||
return vv
|
raise Fatal('Unable to open subnet file: %s' % s)
|
||||||
except ValueError:
|
|
||||||
pass
|
raw_config_lines = handle.readlines()
|
||||||
return v
|
subnets = []
|
||||||
|
for _, line in enumerate(raw_config_lines):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line[0] == '#':
|
||||||
|
continue
|
||||||
|
subnets.append(parse_subnetport(line))
|
||||||
|
|
||||||
|
return subnets
|
||||||
|
|
||||||
|
|
||||||
def _atoi(v):
|
# 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
|
||||||
|
def parse_subnetport(s):
|
||||||
|
if s.count(':') > 1:
|
||||||
|
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
|
||||||
|
else:
|
||||||
|
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()
|
||||||
try:
|
try:
|
||||||
return int(v or 0)
|
addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
|
||||||
except ValueError:
|
except socket.gaierror:
|
||||||
return 0
|
raise Fatal('Unable to resolve address: %s' % addr)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
|
||||||
|
|
||||||
|
|
||||||
def _remove_negative_kv(k, v):
|
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
||||||
if k.startswith('no-') or k.startswith('no_'):
|
# [1:2::3]:456 or [1:2::3] or just [::]:567
|
||||||
return k[3:], not v
|
# example.com:123 or just example.com
|
||||||
return k, v
|
def parse_ipport(s):
|
||||||
|
s = str(s)
|
||||||
|
if s.isdigit():
|
||||||
|
rx = r'()(\d+)$'
|
||||||
|
elif ']' in s:
|
||||||
|
rx = r'(?:\[([^]]+)])(?::(\d+))?$'
|
||||||
|
else:
|
||||||
|
rx = r'([\w\.\-]+)(?::(\d+))?$'
|
||||||
|
|
||||||
|
m = re.match(rx, s)
|
||||||
|
if not m:
|
||||||
|
raise Fatal('%r is not a valid IP:port format' % s)
|
||||||
|
|
||||||
def _remove_negative_k(k):
|
ip, port = m.groups()
|
||||||
return _remove_negative_kv(k, None)[0]
|
ip = ip or '0.0.0.0'
|
||||||
|
port = int(port or 0)
|
||||||
|
|
||||||
|
|
||||||
def _tty_width():
|
|
||||||
if not hasattr(sys.stderr, "fileno"):
|
|
||||||
return _atoi(os.environ.get('WIDTH')) or 70
|
|
||||||
s = struct.pack("HHHH", 0, 0, 0, 0)
|
|
||||||
try:
|
try:
|
||||||
import fcntl
|
addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
|
||||||
import termios
|
except socket.gaierror:
|
||||||
s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
|
raise Fatal('%r is not a valid IP:port format' % s)
|
||||||
except (IOError, ImportError):
|
|
||||||
return _atoi(os.environ.get('WIDTH')) or 70
|
family, _, _, _, addr = min(addrinfo)
|
||||||
(ysize, xsize, ypix, xpix) = struct.unpack('HHHH', s)
|
return (family,) + addr[:2]
|
||||||
return xsize or 70
|
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
def parse_list(lst):
|
||||||
|
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
||||||
|
|
||||||
"""Option parser.
|
|
||||||
When constructed, two strings are mandatory. The first one is the command
|
|
||||||
name showed before error messages. The second one is a string called an
|
|
||||||
optspec that specifies the synopsis and option flags and their description.
|
|
||||||
For more information about optspecs, consult the bup-options(1) man page.
|
|
||||||
|
|
||||||
Two optional arguments specify an alternative parsing function and an
|
class Concat(Action):
|
||||||
alternative behaviour on abort (after having output the usage string).
|
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||||
|
if nargs is not None:
|
||||||
|
raise ValueError("nargs not supported")
|
||||||
|
super(Concat, self).__init__(option_strings, dest, **kwargs)
|
||||||
|
|
||||||
By default, the parser function is getopt.gnu_getopt, and the abort
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
behaviour is to exit the program.
|
curr_value = getattr(namespace, self.dest, None) or []
|
||||||
|
setattr(namespace, self.dest, curr_value + values)
|
||||||
|
|
||||||
|
|
||||||
|
parser = ArgumentParser(
|
||||||
|
prog="sshuttle",
|
||||||
|
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
|
||||||
|
fromfile_prefix_chars="@"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"subnets",
|
||||||
|
metavar="IP/MASK[:PORT[-PORT]]...",
|
||||||
|
nargs="*",
|
||||||
|
type=parse_subnetport,
|
||||||
|
help="""
|
||||||
|
capture and forward traffic to these subnets (whitespace separated)
|
||||||
"""
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-l", "--listen",
|
||||||
|
metavar="[IP:]PORT",
|
||||||
|
help="""
|
||||||
|
transproxy to this ip address and port number
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-H", "--auto-hosts",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
continuously scan for remote hostnames and update local /etc/hosts as
|
||||||
|
they are found
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-N", "--auto-nets",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
automatically determine subnets to route
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dns",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
capture local DNS requests and forward to the remote DNS server
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ns-hosts",
|
||||||
|
metavar="IP[,IP]",
|
||||||
|
default=[],
|
||||||
|
type=parse_list,
|
||||||
|
help="""
|
||||||
|
capture and forward DNS requests made to the following servers
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--to-ns",
|
||||||
|
metavar="IP[:PORT]",
|
||||||
|
type=parse_ipport,
|
||||||
|
help="""
|
||||||
|
the DNS server to forward requests to; defaults to servers in
|
||||||
|
/etc/resolv.conf on remote side if not given.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, optspec, optfunc=getopt.gnu_getopt,
|
parser.add_argument(
|
||||||
onabort=_default_onabort):
|
"--method",
|
||||||
self.optspec = optspec
|
choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
|
||||||
self._onabort = onabort
|
metavar="TYPE",
|
||||||
self.optfunc = optfunc
|
default="auto",
|
||||||
self._aliases = {}
|
help="""
|
||||||
self._shortopts = 'h?'
|
%(choices)s
|
||||||
self._longopts = ['help']
|
"""
|
||||||
self._hasparms = {}
|
)
|
||||||
self._defaults = {}
|
parser.add_argument(
|
||||||
self._usagestr = self._gen_usage()
|
"--python",
|
||||||
|
metavar="PATH",
|
||||||
def _gen_usage(self):
|
help="""
|
||||||
out = []
|
path to python interpreter on the remote server
|
||||||
lines = self.optspec.strip().split('\n')
|
"""
|
||||||
lines.reverse()
|
)
|
||||||
first_syn = True
|
parser.add_argument(
|
||||||
while lines:
|
"-r", "--remote",
|
||||||
l = lines.pop()
|
metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
|
||||||
if l == '--':
|
help="""
|
||||||
break
|
ssh hostname (and optional username and password) of remote %(prog)s server
|
||||||
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
|
"""
|
||||||
first_syn = False
|
)
|
||||||
out.append('\n')
|
parser.add_argument(
|
||||||
last_was_option = False
|
"-x", "--exclude",
|
||||||
while lines:
|
metavar="IP/MASK[:PORT[-PORT]]",
|
||||||
l = lines.pop()
|
action="append",
|
||||||
if l.startswith(' '):
|
default=[],
|
||||||
out.append('%s%s\n' % (last_was_option and '\n' or '',
|
type=parse_subnetport,
|
||||||
l.lstrip()))
|
help="""
|
||||||
last_was_option = False
|
exclude this subnet (can be used more than once)
|
||||||
elif l:
|
"""
|
||||||
(flags, extra) = l.split(' ', 1)
|
)
|
||||||
extra = extra.strip()
|
parser.add_argument(
|
||||||
if flags.endswith('='):
|
"-X", "--exclude-from",
|
||||||
flags = flags[:-1]
|
metavar="PATH",
|
||||||
has_parm = 1
|
action=Concat,
|
||||||
else:
|
dest="exclude",
|
||||||
has_parm = 0
|
type=parse_subnetport_file,
|
||||||
g = re.search(r'\[([^\]]*)\]$', extra)
|
help="""
|
||||||
if g:
|
exclude the subnets in a file (whitespace separated)
|
||||||
defval = g.group(1)
|
"""
|
||||||
else:
|
)
|
||||||
defval = None
|
parser.add_argument(
|
||||||
flagl = flags.split(',')
|
"-v", "--verbose",
|
||||||
flagl_nice = []
|
action="count",
|
||||||
for _f in flagl:
|
default=0,
|
||||||
f, dvi = _remove_negative_kv(_f, _intify(defval))
|
help="""
|
||||||
self._aliases[f] = _remove_negative_k(flagl[0])
|
increase debug message verbosity
|
||||||
self._hasparms[f] = has_parm
|
"""
|
||||||
self._defaults[f] = dvi
|
)
|
||||||
if len(f) == 1:
|
parser.add_argument(
|
||||||
self._shortopts += f + (has_parm and ':' or '')
|
"-V", "--version",
|
||||||
flagl_nice.append('-' + f)
|
action="version",
|
||||||
else:
|
version=__version__,
|
||||||
f_nice = re.sub(r'\W', '_', f)
|
help="""
|
||||||
self._aliases[f_nice] = _remove_negative_k(flagl[0])
|
print the %(prog)s version number and exit
|
||||||
self._longopts.append(f + (has_parm and '=' or ''))
|
"""
|
||||||
self._longopts.append('no-' + f)
|
)
|
||||||
flagl_nice.append('--' + _f)
|
parser.add_argument(
|
||||||
flags_nice = ', '.join(flagl_nice)
|
"-e", "--ssh-cmd",
|
||||||
if has_parm:
|
metavar="CMD",
|
||||||
flags_nice += ' ...'
|
default="ssh",
|
||||||
prefix = ' %-20s ' % flags_nice
|
help="""
|
||||||
argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
|
the command to use to connect to the remote [%(default)s]
|
||||||
initial_indent=prefix,
|
"""
|
||||||
subsequent_indent=' ' * 28))
|
)
|
||||||
out.append(argtext + '\n')
|
parser.add_argument(
|
||||||
last_was_option = True
|
"--seed-hosts",
|
||||||
else:
|
metavar="HOSTNAME[,HOSTNAME]",
|
||||||
out.append('\n')
|
default=[],
|
||||||
last_was_option = False
|
help="""
|
||||||
return ''.join(out).rstrip() + '\n'
|
comma-separated list of hostnames for initial scan (may be used with
|
||||||
|
or without --auto-hosts)
|
||||||
def usage(self, msg=""):
|
"""
|
||||||
"""Print usage string to stderr and abort."""
|
)
|
||||||
sys.stderr.write(self._usagestr)
|
parser.add_argument(
|
||||||
e = self._onabort and self._onabort(msg) or None
|
"--no-latency-control",
|
||||||
if e:
|
action="store_false",
|
||||||
raise e
|
dest="latency_control",
|
||||||
|
help="""
|
||||||
def fatal(self, s):
|
sacrifice latency to improve bandwidth benchmarks
|
||||||
"""Print an error message to stderr and abort with usage string."""
|
"""
|
||||||
msg = 'error: %s\n' % s
|
)
|
||||||
sys.stderr.write(msg)
|
parser.add_argument(
|
||||||
return self.usage(msg)
|
"--latency-buffer-size",
|
||||||
|
metavar="SIZE",
|
||||||
def parse(self, args):
|
type=int,
|
||||||
"""Parse a list of arguments and return (options, flags, extra).
|
default=32768,
|
||||||
|
dest="latency_buffer_size",
|
||||||
In the returned tuple, "options" is an OptDict with known options,
|
help="""
|
||||||
"flags" is a list of option flags that were used on the command-line,
|
size of latency control buffer
|
||||||
and "extra" is a list of positional arguments.
|
"""
|
||||||
"""
|
)
|
||||||
try:
|
parser.add_argument(
|
||||||
(flags, extra) = self.optfunc(
|
"--wrap",
|
||||||
args, self._shortopts, self._longopts)
|
metavar="NUM",
|
||||||
except getopt.GetoptError as e:
|
type=int,
|
||||||
self.fatal(e)
|
help="""
|
||||||
|
restart counting channel numbers after this number (for testing)
|
||||||
opt = OptDict()
|
"""
|
||||||
|
)
|
||||||
for k, v in self._defaults.items():
|
parser.add_argument(
|
||||||
k = self._aliases[k]
|
"--disable-ipv6",
|
||||||
opt[k] = v
|
action="store_true",
|
||||||
|
help="""
|
||||||
for (k, v) in flags:
|
disable IPv6 support
|
||||||
k = k.lstrip('-')
|
"""
|
||||||
if k in ('h', '?', 'help'):
|
)
|
||||||
self.usage()
|
parser.add_argument(
|
||||||
if k.startswith('no-'):
|
"-D", "--daemon",
|
||||||
k = self._aliases[k[3:]]
|
action="store_true",
|
||||||
v = 0
|
help="""
|
||||||
else:
|
run in the background as a daemon
|
||||||
k = self._aliases[k]
|
"""
|
||||||
if not self._hasparms[k]:
|
)
|
||||||
assert(v == '')
|
parser.add_argument(
|
||||||
v = (opt._opts.get(k) or 0) + 1
|
"-s", "--subnets",
|
||||||
else:
|
metavar="PATH",
|
||||||
v = _intify(v)
|
action=Concat,
|
||||||
opt[k] = v
|
dest="subnets_file",
|
||||||
for (f1, f2) in self._aliases.items():
|
default=[],
|
||||||
opt[f1] = opt._opts.get(f2)
|
type=parse_subnetport_file,
|
||||||
return (opt, flags, extra)
|
help="""
|
||||||
|
file where the subnets are stored, instead of on the command line
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--syslog",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
send log messages to syslog (default if you use --daemon)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pidfile",
|
||||||
|
metavar="PATH",
|
||||||
|
default="./sshuttle.pid",
|
||||||
|
help="""
|
||||||
|
pidfile name (only if using --daemon) [%(default)s]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--user",
|
||||||
|
help="""
|
||||||
|
apply all the rules only to this linux user
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--firewall",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
(internal use only)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hostwatch",
|
||||||
|
action="store_true",
|
||||||
|
help="""
|
||||||
|
(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
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
46
sshuttle/sdnotify.py
Normal file
46
sshuttle/sdnotify.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import socket
|
||||||
|
import os
|
||||||
|
|
||||||
|
from sshuttle.helpers import debug1
|
||||||
|
|
||||||
|
|
||||||
|
def _notify(message):
|
||||||
|
addr = os.environ.get("NOTIFY_SOCKET", None)
|
||||||
|
|
||||||
|
if not addr or len(addr) == 1 or addr[0] not in ('/', '@'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
addr = '\0' + addr[1:] if addr[0] == '@' else addr
|
||||||
|
|
||||||
|
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)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return False
|
||||||
|
|
||||||
|
assert isinstance(message, bytes)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return (sock.sendto(message, addr) > 0)
|
||||||
|
except (OSError, IOError) as e:
|
||||||
|
debug1("Error notifying systemd: %s\n" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def send(*messages):
|
||||||
|
return _notify(b'\n'.join(messages))
|
||||||
|
|
||||||
|
|
||||||
|
def ready():
|
||||||
|
return b"READY=1"
|
||||||
|
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
return b"STOPPING=1"
|
||||||
|
|
||||||
|
|
||||||
|
def status(message):
|
||||||
|
return b"STATUS=%s" % message.encode('utf8')
|
@ -6,38 +6,41 @@ import time
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
import sshuttle.ssnet as ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
import sshuttle.hostwatch as hostwatch
|
import sshuttle.hostwatch as hostwatch
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, \
|
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
|
||||||
resolvconf_random_nameserver
|
resolvconf_random_nameserver
|
||||||
|
|
||||||
|
|
||||||
def _ipmatch(ipstr):
|
def _ipmatch(ipstr):
|
||||||
if ipstr == b'default':
|
# FIXME: IPv4 only
|
||||||
ipstr = b'0.0.0.0/0'
|
if ipstr == 'default':
|
||||||
m = re.match(b'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
ipstr = '0.0.0.0/0'
|
||||||
|
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
|
||||||
if m:
|
if m:
|
||||||
g = m.groups()
|
g = m.groups()
|
||||||
ips = g[0]
|
ips = g[0]
|
||||||
width = int(g[4] or 32)
|
width = int(g[4] or 32)
|
||||||
if g[1] is None:
|
if g[1] is None:
|
||||||
ips += b'.0.0.0'
|
ips += '.0.0.0'
|
||||||
width = min(width, 8)
|
width = min(width, 8)
|
||||||
elif g[2] is None:
|
elif g[2] is None:
|
||||||
ips += b'.0.0'
|
ips += '.0.0'
|
||||||
width = min(width, 16)
|
width = min(width, 16)
|
||||||
elif g[3] is None:
|
elif g[3] is None:
|
||||||
ips += b'.0'
|
ips += '.0'
|
||||||
width = min(width, 24)
|
width = min(width, 24)
|
||||||
ips = ips.decode("ASCII")
|
ips = ips
|
||||||
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
|
||||||
|
|
||||||
|
|
||||||
def _ipstr(ip, width):
|
def _ipstr(ip, width):
|
||||||
|
# FIXME: IPv4 only
|
||||||
if width >= 32:
|
if width >= 32:
|
||||||
return ip
|
return ip
|
||||||
else:
|
else:
|
||||||
@ -45,6 +48,7 @@ def _ipstr(ip, width):
|
|||||||
|
|
||||||
|
|
||||||
def _maskbits(netmask):
|
def _maskbits(netmask):
|
||||||
|
# FIXME: IPv4 only
|
||||||
if not netmask:
|
if not netmask:
|
||||||
return 32
|
return 32
|
||||||
for i in range(32):
|
for i in range(32):
|
||||||
@ -57,18 +61,39 @@ def _shl(n, bits):
|
|||||||
return n * int(2 ** bits)
|
return n * int(2 ** bits)
|
||||||
|
|
||||||
|
|
||||||
def _list_routes():
|
def _route_netstat(line):
|
||||||
|
cols = line.split(None)
|
||||||
|
if len(cols) < 3:
|
||||||
|
return None, None
|
||||||
|
ipw = _ipmatch(cols[0])
|
||||||
|
maskw = _ipmatch(cols[2]) # linux only
|
||||||
|
mask = _maskbits(maskw) # returns 32 if maskw is null
|
||||||
|
return ipw, mask
|
||||||
|
|
||||||
|
|
||||||
|
def _route_iproute(line):
|
||||||
|
ipm = line.split(None, 1)[0]
|
||||||
|
if '/' not in ipm:
|
||||||
|
return None, None
|
||||||
|
ip, mask = ipm.split('/')
|
||||||
|
ipw = _ipmatch(ip)
|
||||||
|
return ipw, int(mask)
|
||||||
|
|
||||||
|
|
||||||
|
def _list_routes(argv, extract_route):
|
||||||
# FIXME: IPv4 only
|
# FIXME: IPv4 only
|
||||||
argv = ['netstat', '-rn']
|
env = {
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
'PATH': os.environ['PATH'],
|
||||||
|
'LC_ALL': "C",
|
||||||
|
}
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
|
||||||
routes = []
|
routes = []
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
cols = re.split(b'\s+', line)
|
if not line.strip():
|
||||||
ipw = _ipmatch(cols[0])
|
continue
|
||||||
|
ipw, mask = extract_route(line.decode("ASCII"))
|
||||||
if not ipw:
|
if not ipw:
|
||||||
continue # some lines won't be parseable; never mind
|
continue
|
||||||
maskw = _ipmatch(cols[2]) # linux only
|
|
||||||
mask = _maskbits(maskw) # returns 32 if maskw is null
|
|
||||||
width = min(ipw[1], mask)
|
width = min(ipw[1], mask)
|
||||||
ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width)
|
ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width)
|
||||||
routes.append(
|
routes.append(
|
||||||
@ -77,11 +102,20 @@ def _list_routes():
|
|||||||
if rv != 0:
|
if rv != 0:
|
||||||
log('WARNING: %r returned %d\n' % (argv, rv))
|
log('WARNING: %r returned %d\n' % (argv, rv))
|
||||||
log('WARNING: That prevents --auto-nets from working.\n')
|
log('WARNING: That prevents --auto-nets from working.\n')
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
|
|
||||||
def list_routes():
|
def list_routes():
|
||||||
for (family, ip, width) in _list_routes():
|
if which('ip'):
|
||||||
|
routes = _list_routes(['ip', 'route'], _route_iproute)
|
||||||
|
elif which('netstat'):
|
||||||
|
routes = _list_routes(['netstat', '-rn'], _route_netstat)
|
||||||
|
else:
|
||||||
|
log('WARNING: Neither ip nor netstat were found on the server.\n')
|
||||||
|
routes = []
|
||||||
|
|
||||||
|
for (family, ip, width) in routes:
|
||||||
if not ip.startswith('0.') and not ip.startswith('127.'):
|
if not ip.startswith('0.') and not ip.startswith('127.'):
|
||||||
yield (family, ip, width)
|
yield (family, ip, width)
|
||||||
|
|
||||||
@ -91,7 +125,7 @@ def _exc_dump():
|
|||||||
return ''.join(traceback.format_exception(*exc_info))
|
return ''.join(traceback.format_exception(*exc_info))
|
||||||
|
|
||||||
|
|
||||||
def start_hostwatch(seed_hosts):
|
def start_hostwatch(seed_hosts, auto_hosts):
|
||||||
s1, s2 = socket.socketpair()
|
s1, s2 = socket.socketpair()
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if not pid:
|
if not pid:
|
||||||
@ -103,7 +137,7 @@ def start_hostwatch(seed_hosts):
|
|||||||
os.dup2(s1.fileno(), 1)
|
os.dup2(s1.fileno(), 1)
|
||||||
os.dup2(s1.fileno(), 0)
|
os.dup2(s1.fileno(), 0)
|
||||||
s1.close()
|
s1.close()
|
||||||
rv = hostwatch.hw_main(seed_hosts) or 0
|
rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
log('%s\n' % _exc_dump())
|
log('%s\n' % _exc_dump())
|
||||||
rv = 98
|
rv = 98
|
||||||
@ -122,7 +156,7 @@ class Hostwatch:
|
|||||||
|
|
||||||
class DnsProxy(Handler):
|
class DnsProxy(Handler):
|
||||||
|
|
||||||
def __init__(self, mux, chan, request):
|
def __init__(self, mux, chan, request, to_nameserver):
|
||||||
Handler.__init__(self, [])
|
Handler.__init__(self, [])
|
||||||
self.timeout = time.time() + 30
|
self.timeout = time.time() + 30
|
||||||
self.mux = mux
|
self.mux = mux
|
||||||
@ -130,26 +164,48 @@ class DnsProxy(Handler):
|
|||||||
self.tries = 0
|
self.tries = 0
|
||||||
self.request = request
|
self.request = request
|
||||||
self.peers = {}
|
self.peers = {}
|
||||||
|
self.to_ns_peer = None
|
||||||
|
self.to_ns_port = None
|
||||||
|
if to_nameserver is None:
|
||||||
|
self.to_nameserver = None
|
||||||
|
else:
|
||||||
|
self.to_ns_peer, self.to_ns_port = to_nameserver.split("@")
|
||||||
|
self.to_nameserver = self._addrinfo(self.to_ns_peer,
|
||||||
|
self.to_ns_port)
|
||||||
self.try_send()
|
self.try_send()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _addrinfo(peer, port):
|
||||||
|
if int(port) == 0:
|
||||||
|
port = 53
|
||||||
|
family, _, _, _, sockaddr = socket.getaddrinfo(peer, port)[0]
|
||||||
|
return (family, sockaddr)
|
||||||
|
|
||||||
def try_send(self):
|
def try_send(self):
|
||||||
if self.tries >= 3:
|
if self.tries >= 3:
|
||||||
return
|
return
|
||||||
self.tries += 1
|
self.tries += 1
|
||||||
|
|
||||||
family, peer = resolvconf_random_nameserver()
|
if self.to_nameserver is None:
|
||||||
|
_, peer = resolvconf_random_nameserver()
|
||||||
|
port = 53
|
||||||
|
else:
|
||||||
|
peer = self.to_ns_peer
|
||||||
|
port = int(self.to_ns_port)
|
||||||
|
|
||||||
|
family, sockaddr = self._addrinfo(peer, port)
|
||||||
sock = socket.socket(family, socket.SOCK_DGRAM)
|
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||||
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
sock.connect((peer, 53))
|
sock.connect(sockaddr)
|
||||||
|
|
||||||
self.peers[sock] = peer
|
self.peers[sock] = peer
|
||||||
|
|
||||||
debug2('DNS: sending to %r (try %d)\n' % (peer, self.tries))
|
debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries))
|
||||||
try:
|
try:
|
||||||
sock.send(self.request)
|
sock.send(self.request)
|
||||||
self.socks.append(sock)
|
self.socks.append(sock)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.args[0] in ssnet.NET_ERRS:
|
if e.args[0] in ssnet.NET_ERRS:
|
||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
@ -166,7 +222,8 @@ class DnsProxy(Handler):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data = sock.recv(4096)
|
data = sock.recv(4096)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
self.socks.remove(sock)
|
self.socks.remove(sock)
|
||||||
del self.peers[sock]
|
del self.peers[sock]
|
||||||
|
|
||||||
@ -201,22 +258,24 @@ class UdpProxy(Handler):
|
|||||||
debug2('UDP: sending to %r port %d\n' % dstip)
|
debug2('UDP: sending to %r port %d\n' % dstip)
|
||||||
try:
|
try:
|
||||||
self.sock.sendto(data, dstip)
|
self.sock.sendto(data, dstip)
|
||||||
except socket.error as e:
|
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\n' % (dstip[0], dstip[1], e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
try:
|
try:
|
||||||
data, peer = sock.recvfrom(4096)
|
data, peer = sock.recvfrom(4096)
|
||||||
except socket.error as e:
|
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\n' % (peer[0], peer[1], e))
|
||||||
return
|
return
|
||||||
debug2('UDP response: %d bytes\n' % len(data))
|
debug2('UDP response: %d bytes\n' % len(data))
|
||||||
hdr = "%s,%r," % (peer[0], peer[1])
|
hdr = b("%s,%r," % (peer[0], peer[1]))
|
||||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||||
|
|
||||||
|
|
||||||
def main(latency_control):
|
def main(latency_control, auto_hosts, to_nameserver, auto_nets):
|
||||||
debug1('Starting server with Python version %s\n'
|
debug1('Starting server with Python version %s\n'
|
||||||
% platform.python_version())
|
% platform.python_version())
|
||||||
|
|
||||||
@ -226,54 +285,63 @@ def main(latency_control):
|
|||||||
helpers.logprefix = 'server: '
|
helpers.logprefix = 'server: '
|
||||||
debug1('latency control setting = %r\n' % latency_control)
|
debug1('latency control setting = %r\n' % latency_control)
|
||||||
|
|
||||||
routes = list(list_routes())
|
|
||||||
debug1('available routes:\n')
|
|
||||||
for r in routes:
|
|
||||||
debug1(' %d/%s/%d\n' % r)
|
|
||||||
|
|
||||||
# synchronization header
|
# synchronization header
|
||||||
sys.stdout.write('\0\0SSHUTTLE0001')
|
sys.stdout.write('\0\0SSHUTTLE0001')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
mux = Mux(socket.fromfd(sys.stdin.fileno(),
|
mux = Mux(sys.stdin, sys.stdout)
|
||||||
socket.AF_INET, socket.SOCK_STREAM),
|
|
||||||
socket.fromfd(sys.stdout.fileno(),
|
|
||||||
socket.AF_INET, socket.SOCK_STREAM))
|
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
routepkt = b''
|
|
||||||
|
debug1('auto-nets:' + str(auto_nets) + '\n')
|
||||||
|
if auto_nets:
|
||||||
|
routes = list(list_routes())
|
||||||
|
debug1('available routes:\n')
|
||||||
|
for r in routes:
|
||||||
|
debug1(' %d/%s/%d\n' % r)
|
||||||
|
else:
|
||||||
|
routes = []
|
||||||
|
|
||||||
|
routepkt = ''
|
||||||
for r in routes:
|
for r in routes:
|
||||||
routepkt += b'%d,%s,%d\n' % (r[0], r[1].encode("ASCII"), r[2])
|
routepkt += '%d,%s,%d\n' % r
|
||||||
mux.send(0, ssnet.CMD_ROUTES, routepkt)
|
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
|
||||||
|
|
||||||
hw = Hostwatch()
|
hw = Hostwatch()
|
||||||
hw.leftover = b''
|
hw.leftover = b('')
|
||||||
|
|
||||||
def hostwatch_ready(sock):
|
def hostwatch_ready(sock):
|
||||||
assert(hw.pid)
|
assert(hw.pid)
|
||||||
content = hw.sock.recv(4096)
|
content = hw.sock.recv(4096)
|
||||||
if content:
|
if content:
|
||||||
lines = (hw.leftover + content).split(b'\n')
|
lines = (hw.leftover + content).split(b('\n'))
|
||||||
if lines[-1]:
|
if lines[-1]:
|
||||||
# no terminating newline: entry isn't complete yet!
|
# no terminating newline: entry isn't complete yet!
|
||||||
hw.leftover = lines.pop()
|
hw.leftover = lines.pop()
|
||||||
lines.append('')
|
lines.append(b(''))
|
||||||
else:
|
else:
|
||||||
hw.leftover = b''
|
hw.leftover = b('')
|
||||||
mux.send(0, ssnet.CMD_HOST_LIST, b'\n'.join(lines))
|
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
|
||||||
else:
|
else:
|
||||||
raise Fatal('hostwatch process died')
|
raise Fatal('hostwatch process died')
|
||||||
|
|
||||||
def got_host_req(data):
|
def got_host_req(data):
|
||||||
if not hw.pid:
|
if not hw.pid:
|
||||||
(hw.pid, hw.sock) = start_hostwatch(data.strip().split())
|
(hw.pid, hw.sock) = start_hostwatch(
|
||||||
|
data.decode("ASCII").strip().split(), auto_hosts)
|
||||||
handlers.append(Handler(socks=[hw.sock],
|
handlers.append(Handler(socks=[hw.sock],
|
||||||
callback=hostwatch_ready))
|
callback=hostwatch_ready))
|
||||||
mux.got_host_req = got_host_req
|
mux.got_host_req = got_host_req
|
||||||
|
|
||||||
def new_channel(channel, data):
|
def new_channel(channel, data):
|
||||||
(family, dstip, dstport) = data.split(b',', 2)
|
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
|
||||||
family = int(family)
|
family = int(family)
|
||||||
|
# AF_INET is the same constant on Linux and BSD but AF_INET6
|
||||||
|
# is different. As the client and server can be running on
|
||||||
|
# different platforms we can not just set the socket family
|
||||||
|
# to what comes in the wire.
|
||||||
|
if family != socket.AF_INET:
|
||||||
|
family = socket.AF_INET6
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
outwrap = ssnet.connect_dst(family, dstip, dstport)
|
||||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||||
@ -283,7 +351,7 @@ def main(latency_control):
|
|||||||
|
|
||||||
def dns_req(channel, data):
|
def dns_req(channel, data):
|
||||||
debug2('Incoming DNS request channel=%d.\n' % channel)
|
debug2('Incoming DNS request channel=%d.\n' % channel)
|
||||||
h = DnsProxy(mux, channel, data)
|
h = DnsProxy(mux, channel, data, to_nameserver)
|
||||||
handlers.append(h)
|
handlers.append(h)
|
||||||
dnshandlers[channel] = h
|
dnshandlers[channel] = h
|
||||||
mux.got_dns_req = dns_req
|
mux.got_dns_req = dns_req
|
||||||
@ -293,7 +361,7 @@ def main(latency_control):
|
|||||||
def udp_req(channel, cmd, data):
|
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\n' % (channel, cmd))
|
||||||
if cmd == ssnet.CMD_UDP_DATA:
|
if cmd == ssnet.CMD_UDP_DATA:
|
||||||
(dstip, dstport, data) = data.split(",", 2)
|
(dstip, dstport, data) = data.split(b(','), 2)
|
||||||
dstport = int(dstport)
|
dstport = int(dstport)
|
||||||
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
|
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
|
||||||
h = udphandlers[channel]
|
h = udphandlers[channel]
|
||||||
|
160
sshuttle/ssh.py
160
sshuttle/ssh.py
@ -3,81 +3,94 @@ import os
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import zlib
|
import zlib
|
||||||
import imp
|
import importlib
|
||||||
|
import importlib.util
|
||||||
import subprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
|
import shlex
|
||||||
|
from shlex import quote
|
||||||
|
import ipaddress
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import sshuttle.helpers as helpers
|
import sshuttle.helpers as helpers
|
||||||
from sshuttle.helpers import debug2
|
from sshuttle.helpers import debug2
|
||||||
|
|
||||||
|
|
||||||
def readfile(name):
|
def get_module_source(name):
|
||||||
tokens = name.split(".")
|
spec = importlib.util.find_spec(name)
|
||||||
f = None
|
with open(spec.origin, "rt") as f:
|
||||||
|
return f.read().encode("utf-8")
|
||||||
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 empackage(z, name, data=None):
|
def empackage(z, name, data=None):
|
||||||
if not data:
|
if not data:
|
||||||
data = readfile(name)
|
data = get_module_source(name)
|
||||||
content = z.compress(data)
|
content = z.compress(data)
|
||||||
content += z.flush(zlib.Z_SYNC_FLUSH)
|
content += z.flush(zlib.Z_SYNC_FLUSH)
|
||||||
|
|
||||||
return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content)
|
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
|
||||||
|
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 adress with port specification,
|
||||||
|
# then it will look like: [::1]:22
|
||||||
|
|
||||||
|
try:
|
||||||
|
# try to parse host as an IP adress,
|
||||||
|
# if that works it is an IPv6 address
|
||||||
|
host = ipaddress.ip_address(host)
|
||||||
|
except ValueError:
|
||||||
|
# if that fails parse as URL to get the port
|
||||||
|
parsed = urlparse('//{}'.format(host))
|
||||||
|
try:
|
||||||
|
host = ipaddress.ip_address(parsed.hostname)
|
||||||
|
except ValueError:
|
||||||
|
# else if both fails, we have a hostname with port
|
||||||
|
host = parsed.hostname
|
||||||
|
port = parsed.port
|
||||||
|
|
||||||
|
if password is None or len(password) == 0:
|
||||||
|
password = None
|
||||||
|
|
||||||
|
return username, password, port, host
|
||||||
|
|
||||||
|
|
||||||
def connect(ssh_cmd, rhostport, python, stderr, options):
|
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||||
portl = []
|
username, password, port, host = parse_hostport(rhostport)
|
||||||
|
if username:
|
||||||
if (rhostport or '').count(':') > 1:
|
rhost = "{}@{}".format(username, host)
|
||||||
if rhostport.count(']') or rhostport.count('['):
|
else:
|
||||||
result = rhostport.split(']')
|
rhost = host
|
||||||
rhost = result[0].strip('[')
|
|
||||||
if len(result) > 1:
|
|
||||||
result[1] = result[1].strip(':')
|
|
||||||
if result[1] is not '':
|
|
||||||
portl = ['-p', str(int(result[1]))]
|
|
||||||
# can't disambiguate IPv6 colons and a port number. pass the hostname
|
|
||||||
# through.
|
|
||||||
else:
|
|
||||||
rhost = rhostport
|
|
||||||
else: # IPv4
|
|
||||||
l = (rhostport or '').split(':', 1)
|
|
||||||
rhost = l[0]
|
|
||||||
if len(l) > 1:
|
|
||||||
portl = ['-p', str(int(l[1]))]
|
|
||||||
|
|
||||||
if rhost == '-':
|
|
||||||
rhost = None
|
|
||||||
|
|
||||||
z = zlib.compressobj(1)
|
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 = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
|
||||||
optdata = optdata.encode("UTF8")
|
optdata = optdata.encode("UTF8")
|
||||||
content2 = (empackage(z, 'sshuttle') +
|
content2 = (empackage(z, 'sshuttle') +
|
||||||
@ -89,30 +102,43 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
b"\n")
|
b"\n")
|
||||||
|
|
||||||
pyscript = r"""
|
pyscript = r"""
|
||||||
import sys;
|
import sys, os;
|
||||||
verbosity=%d;
|
verbosity=%d;
|
||||||
stdin=getattr(sys.stdin,"buffer",sys.stdin);
|
sys.stdin = os.fdopen(0, "rb");
|
||||||
exec(compile(stdin.read(%d), "assembler.py", "exec"))
|
exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
|
||||||
""" % (helpers.verbose or 0, len(content))
|
""" % (helpers.verbose or 0, len(content))
|
||||||
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
|
||||||
|
|
||||||
if not rhost:
|
if not rhost:
|
||||||
# ignore the --python argument when running locally; we already know
|
# ignore the --python argument when running locally; we already know
|
||||||
# which python version works.
|
# which python version works.
|
||||||
argv = [sys.argv[1], '-c', pyscript]
|
argv = [sys.executable, '-c', pyscript]
|
||||||
else:
|
else:
|
||||||
if ssh_cmd:
|
if ssh_cmd:
|
||||||
sshl = ssh_cmd.split(' ')
|
sshl = shlex.split(ssh_cmd)
|
||||||
else:
|
else:
|
||||||
sshl = ['ssh']
|
sshl = ['ssh']
|
||||||
|
if port is not None:
|
||||||
|
portl = ["-p", str(port)]
|
||||||
|
else:
|
||||||
|
portl = []
|
||||||
if python:
|
if python:
|
||||||
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
pycmd = "'%s' -c '%s'" % (python, pyscript)
|
||||||
else:
|
else:
|
||||||
pycmd = ("P=python3.5; $P -V 2>/dev/null || P=python; "
|
pycmd = ("P=python3; $P -V 2>%s || P=python; "
|
||||||
"exec \"$P\" -c '%s'") % pyscript
|
"exec \"$P\" -c %s") % (os.devnull, quote(pyscript))
|
||||||
argv = (sshl +
|
pycmd = ("/bin/sh -c {}".format(quote(pycmd)))
|
||||||
portl +
|
|
||||||
[rhost, '--', pycmd])
|
if password is not None:
|
||||||
|
os.environ['SSHPASS'] = str(password)
|
||||||
|
argv = (["sshpass", "-e"] + sshl +
|
||||||
|
portl +
|
||||||
|
[rhost, '--', pycmd])
|
||||||
|
|
||||||
|
else:
|
||||||
|
argv = (sshl +
|
||||||
|
portl +
|
||||||
|
[rhost, '--', pycmd])
|
||||||
(s1, s2) = socket.socketpair()
|
(s1, s2) = socket.socketpair()
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
|
@ -1,261 +0,0 @@
|
|||||||
% sshuttle(8) Sshuttle 0.46
|
|
||||||
% Avery Pennarun <apenwarr@gmail.com>
|
|
||||||
% 2011-01-25
|
|
||||||
|
|
||||||
# NAME
|
|
||||||
|
|
||||||
sshuttle - a transparent proxy-based VPN using ssh
|
|
||||||
|
|
||||||
# SYNOPSIS
|
|
||||||
|
|
||||||
sshuttle [options...] [-r [username@]sshserver[:port]] \<subnets...\>
|
|
||||||
|
|
||||||
|
|
||||||
# DESCRIPTION
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
To work, you must have root access on the local machine,
|
|
||||||
but you can have a normal account on the server.
|
|
||||||
|
|
||||||
It's valid to run sshuttle more than once simultaneously on
|
|
||||||
a single client machine, connecting to a different server
|
|
||||||
every time, so you can be on more than one VPN at once.
|
|
||||||
|
|
||||||
If run on a router, sshuttle can forward traffic for your
|
|
||||||
entire subnet to the VPN.
|
|
||||||
|
|
||||||
|
|
||||||
# OPTIONS
|
|
||||||
|
|
||||||
\<subnets...\>
|
|
||||||
: a list of subnets to route over the VPN, in the form
|
|
||||||
`a.b.c.d[/width]`. 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').
|
|
||||||
|
|
||||||
-l, --listen=*[ip:]port*
|
|
||||||
: use this ip address and port number as the transparent
|
|
||||||
proxy port. By default sshuttle finds an available
|
|
||||||
port automatically and listens on IP 127.0.0.1
|
|
||||||
(localhost), so you don't need to override it, and
|
|
||||||
connections are only proxied from the local machine,
|
|
||||||
not from outside machines. If you want to accept
|
|
||||||
connections from other machines on your network (ie. to
|
|
||||||
run sshuttle on a router) try enabling IP Forwarding in
|
|
||||||
your kernel, then using `--listen 0.0.0.0:0`.
|
|
||||||
|
|
||||||
-H, --auto-hosts
|
|
||||||
: scan for remote hostnames and update the local /etc/hosts
|
|
||||||
file with matching entries for as long as the VPN is
|
|
||||||
open. This is nicer than changing your system's DNS
|
|
||||||
(/etc/resolv.conf) settings, for several reasons. First,
|
|
||||||
hostnames are added without domain names attached, so
|
|
||||||
you can `ssh thatserver` without worrying if your local
|
|
||||||
domain matches the remote one. Second, if you sshuttle
|
|
||||||
into more than one VPN at a time, it's impossible to
|
|
||||||
use more than one DNS server at once anyway, but
|
|
||||||
sshuttle correctly merges /etc/hosts entries between
|
|
||||||
all running copies. Third, if you're only routing a
|
|
||||||
few subnets over the VPN, you probably would prefer to
|
|
||||||
keep using your local DNS server for everything else.
|
|
||||||
|
|
||||||
-N, --auto-nets
|
|
||||||
: in addition to the subnets provided on the command
|
|
||||||
line, ask the server which subnets it thinks we should
|
|
||||||
route, and route those automatically. The suggestions
|
|
||||||
are taken automatically from the server's routing
|
|
||||||
table.
|
|
||||||
|
|
||||||
--dns
|
|
||||||
: capture local DNS requests and forward to the remote DNS
|
|
||||||
server.
|
|
||||||
|
|
||||||
--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.
|
|
||||||
|
|
||||||
-r, --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.
|
|
||||||
|
|
||||||
-x, --exclude=*subnet*
|
|
||||||
: explicitly exclude this subnet from forwarding. The
|
|
||||||
format of this option is the same as the `<subnets>`
|
|
||||||
option. To exclude more than one subnet, specify the
|
|
||||||
`-x` option more than once. You can say something like
|
|
||||||
`0/0 -x 1.2.3.0/24` to forward everything except the
|
|
||||||
local subnet over the VPN, for example.
|
|
||||||
|
|
||||||
-X, --exclude-from=*file*
|
|
||||||
: exclude the subnets specified in a file, one subnet per
|
|
||||||
line. Useful when you have lots of subnets to exclude.
|
|
||||||
|
|
||||||
-v, --verbose
|
|
||||||
: print more information about the session. This option
|
|
||||||
can be used more than once for increased verbosity. By
|
|
||||||
default, sshuttle prints only error messages.
|
|
||||||
|
|
||||||
-e, --ssh-cmd
|
|
||||||
: the command to use to connect to the remote server. The
|
|
||||||
default is just `ssh`. Use this if your ssh client is
|
|
||||||
in a non-standard location or you want to provide extra
|
|
||||||
options to the ssh command, for example, `-e 'ssh -v'`.
|
|
||||||
|
|
||||||
--seed-hosts
|
|
||||||
: a comma-separated list of hostnames to use to
|
|
||||||
initialize the `--auto-hosts` scan algorithm.
|
|
||||||
`--auto-hosts` does things like poll local SMB servers
|
|
||||||
for lists of local hostnames, but can speed things up
|
|
||||||
if you use this option to give it a few names to start
|
|
||||||
from.
|
|
||||||
|
|
||||||
--no-latency-control
|
|
||||||
: sacrifice latency to improve bandwidth benchmarks. ssh
|
|
||||||
uses really big socket buffers, which can overload the
|
|
||||||
connection if you start doing large file transfers,
|
|
||||||
thus making all your other sessions inside the same
|
|
||||||
tunnel go slowly. Normally, sshuttle tries to avoid
|
|
||||||
this problem using a "fullness check" that allows only
|
|
||||||
a certain amount of outstanding data to be buffered at
|
|
||||||
a time. But on high-bandwidth links, this can leave a
|
|
||||||
lot of your bandwidth underutilized. It also makes
|
|
||||||
sshuttle seem slow in bandwidth benchmarks (benchmarks
|
|
||||||
rarely test ping latency, which is what sshuttle is
|
|
||||||
trying to control). This option disables the latency
|
|
||||||
control feature, maximizing bandwidth usage. Use at
|
|
||||||
your own risk.
|
|
||||||
|
|
||||||
-D, --daemon
|
|
||||||
: automatically fork into the background after connecting
|
|
||||||
to the remote server. Implies `--syslog`.
|
|
||||||
|
|
||||||
--syslog
|
|
||||||
: after connecting, send all log messages to the
|
|
||||||
`syslog`(3) service instead of stderr. This is
|
|
||||||
implicit if you use `--daemon`.
|
|
||||||
|
|
||||||
--pidfile=*pidfilename*
|
|
||||||
: when using `--daemon`, save sshuttle's pid to
|
|
||||||
*pidfilename*. The default is `sshuttle.pid` in the
|
|
||||||
current directory.
|
|
||||||
|
|
||||||
--firewall
|
|
||||||
: (internal use only) run the firewall manager. This is
|
|
||||||
the only part of sshuttle that must run as root. If
|
|
||||||
you start sshuttle as a non-root user, it will
|
|
||||||
automatically run `sudo` or `su` to start the firewall
|
|
||||||
manager, but the core of sshuttle still runs as a
|
|
||||||
normal user.
|
|
||||||
|
|
||||||
--hostwatch
|
|
||||||
: (internal use only) run the hostwatch daemon. This
|
|
||||||
process runs on the server side and collects hostnames for
|
|
||||||
the `--auto-hosts` option. Using this option by itself
|
|
||||||
makes it a lot easier to debug and test the `--auto-hosts`
|
|
||||||
feature.
|
|
||||||
|
|
||||||
|
|
||||||
# EXAMPLES
|
|
||||||
|
|
||||||
Test locally by proxying all local connections, without using ssh:
|
|
||||||
|
|
||||||
$ sshuttle -v 0/0
|
|
||||||
|
|
||||||
Starting sshuttle proxy.
|
|
||||||
Listening on ('0.0.0.0', 12300).
|
|
||||||
[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...
|
|
||||||
^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
|
|
||||||
|
|
||||||
Test connection 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...
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# DISCUSSION
|
|
||||||
|
|
||||||
When it starts, 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.
|
|
||||||
|
|
||||||
After connecting to the remote server, sshuttle uploads its
|
|
||||||
(python) source code to the remote end and executes it
|
|
||||||
there. Thus, you don't need to install sshuttle on the
|
|
||||||
remote server, and there are never sshuttle version
|
|
||||||
conflicts between client and server.
|
|
||||||
|
|
||||||
Unlike most VPNs, sshuttle forwards sessions, not packets.
|
|
||||||
That is, it uses kernel transparent proxying (`iptables
|
|
||||||
REDIRECT` rules on Linux) to
|
|
||||||
capture outgoing TCP sessions, then creates entirely
|
|
||||||
separate TCP sessions out to the original destination at
|
|
||||||
the other end of the tunnel.
|
|
||||||
|
|
||||||
Packet-level forwarding (eg. using the tun/tap devices on
|
|
||||||
Linux) seems elegant at first, but it results in
|
|
||||||
several problems, notably the 'tcp over tcp' problem. The
|
|
||||||
tcp protocol depends fundamentally on packets being dropped
|
|
||||||
in order to implement its congestion control agorithm; if
|
|
||||||
you pass tcp packets through a tcp-based tunnel (such as
|
|
||||||
ssh), the inner tcp packets will never be dropped, and so
|
|
||||||
the inner tcp stream's congestion control will be
|
|
||||||
completely broken, and performance will be terrible. Thus,
|
|
||||||
packet-based VPNs (such as IPsec and openvpn) cannot use
|
|
||||||
tcp-based encrypted streams like ssh or ssl, and have to
|
|
||||||
implement their own encryption from scratch, which is very
|
|
||||||
complex and error prone.
|
|
||||||
|
|
||||||
sshuttle's simplicity comes from the fact that it can
|
|
||||||
safely use the existing ssh encrypted tunnel without
|
|
||||||
incurring a performance penalty. It does this by letting
|
|
||||||
the client-side kernel manage the incoming tcp stream, and
|
|
||||||
the server-side kernel manage the outgoing tcp stream;
|
|
||||||
there is no need for congestion control to be shared
|
|
||||||
between the two separate streams, so a tcp-based tunnel is
|
|
||||||
fine.
|
|
||||||
|
|
||||||
|
|
||||||
# SEE ALSO
|
|
||||||
|
|
||||||
`ssh`(1), `python`(1)
|
|
@ -1,21 +1,21 @@
|
|||||||
|
import sys
|
||||||
import struct
|
import struct
|
||||||
import socket
|
import socket
|
||||||
import errno
|
import errno
|
||||||
import select
|
import select
|
||||||
import os
|
import os
|
||||||
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal
|
|
||||||
|
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal
|
||||||
|
|
||||||
MAX_CHANNEL = 65535
|
MAX_CHANNEL = 65535
|
||||||
|
LATENCY_BUFFER_SIZE = 32768
|
||||||
|
|
||||||
# these don't exist in the socket module in python 2.3!
|
|
||||||
SHUT_RD = 0
|
SHUT_RD = 0
|
||||||
SHUT_WR = 1
|
SHUT_WR = 1
|
||||||
SHUT_RDWR = 2
|
SHUT_RDWR = 2
|
||||||
|
|
||||||
|
|
||||||
HDR_LEN = 8
|
HDR_LEN = 8
|
||||||
|
|
||||||
|
|
||||||
CMD_EXIT = 0x4200
|
CMD_EXIT = 0x4200
|
||||||
CMD_PING = 0x4201
|
CMD_PING = 0x4201
|
||||||
CMD_PONG = 0x4202
|
CMD_PONG = 0x4202
|
||||||
@ -53,17 +53,19 @@ cmd_to_name = {
|
|||||||
|
|
||||||
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
||||||
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
errno.EHOSTUNREACH, errno.ENETUNREACH,
|
||||||
errno.EHOSTDOWN, errno.ENETDOWN]
|
errno.EHOSTDOWN, errno.ENETDOWN,
|
||||||
|
errno.ENETUNREACH, errno.ECONNABORTED,
|
||||||
|
errno.ECONNRESET]
|
||||||
|
|
||||||
|
|
||||||
def _add(l, elem):
|
def _add(socks, elem):
|
||||||
if elem not in l:
|
if elem not in socks:
|
||||||
l.append(elem)
|
socks.append(elem)
|
||||||
|
|
||||||
|
|
||||||
def _fds(l):
|
def _fds(socks):
|
||||||
out = []
|
out = []
|
||||||
for i in l:
|
for i in socks:
|
||||||
try:
|
try:
|
||||||
out.append(i.fileno())
|
out.append(i.fileno())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -75,7 +77,8 @@ def _fds(l):
|
|||||||
def _nb_clean(func, *args):
|
def _nb_clean(func, *args):
|
||||||
try:
|
try:
|
||||||
return func(*args)
|
return func(*args)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
@ -88,9 +91,14 @@ def _try_peername(sock):
|
|||||||
pn = sock.getpeername()
|
pn = sock.getpeername()
|
||||||
if pn:
|
if pn:
|
||||||
return '%s:%s' % (pn[0], pn[1])
|
return '%s:%s' % (pn[0], pn[1])
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
_, e = sys.exc_info()[:2]
|
||||||
|
if e.args[0] == errno.EINVAL:
|
||||||
|
pass
|
||||||
|
elif e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
||||||
raise
|
raise
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
@ -144,7 +152,8 @@ class SockWrapper:
|
|||||||
self.rsock.connect(self.connect_to)
|
self.rsock.connect(self.connect_to)
|
||||||
# connected successfully (Linux)
|
# connected successfully (Linux)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
debug3('%r: connect result: %s\n' % (self, e))
|
debug3('%r: connect result: %s\n' % (self, e))
|
||||||
if e.args[0] == errno.EINVAL:
|
if e.args[0] == errno.EINVAL:
|
||||||
# this is what happens when you call connect() on a socket
|
# this is what happens when you call connect() on a socket
|
||||||
@ -191,10 +200,12 @@ class SockWrapper:
|
|||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
try:
|
try:
|
||||||
self.wsock.shutdown(SHUT_WR)
|
self.wsock.shutdown(SHUT_WR)
|
||||||
except socket.error as e:
|
except socket.error:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
self.seterr('nowrite: %s' % e)
|
self.seterr('nowrite: %s' % e)
|
||||||
|
|
||||||
def too_full(self):
|
@staticmethod
|
||||||
|
def too_full():
|
||||||
return False # fullness is determined by the socket's select() state
|
return False # fullness is determined by the socket's select() state
|
||||||
|
|
||||||
def uwrite(self, buf):
|
def uwrite(self, buf):
|
||||||
@ -203,7 +214,8 @@ class SockWrapper:
|
|||||||
self.wsock.setblocking(False)
|
self.wsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
if e.errno == errno.EPIPE:
|
if e.errno == errno.EPIPE:
|
||||||
debug1('%r: uwrite: got EPIPE\n' % self)
|
debug1('%r: uwrite: got EPIPE\n' % self)
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
@ -225,9 +237,10 @@ class SockWrapper:
|
|||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
self.seterr('uread: %s' % e)
|
self.seterr('uread: %s' % e)
|
||||||
return b'' # unexpected error... we'll call it EOF
|
return b('') # unexpected error... we'll call it EOF
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
if self.buf:
|
if self.buf:
|
||||||
@ -235,7 +248,7 @@ class SockWrapper:
|
|||||||
rb = self.uread()
|
rb = self.uread()
|
||||||
if rb:
|
if rb:
|
||||||
self.buf.append(rb)
|
self.buf.append(rb)
|
||||||
if rb == b'': # empty string means EOF; None means temporarily empty
|
if rb == b(''): # empty string means EOF; None means temporarily empty
|
||||||
self.noread()
|
self.noread()
|
||||||
|
|
||||||
def copy_to(self, outwrap):
|
def copy_to(self, outwrap):
|
||||||
@ -262,7 +275,7 @@ class Handler:
|
|||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
log('--no callback defined-- %r\n' % self)
|
log('--no callback defined-- %r\n' % self)
|
||||||
(r, w, x) = select.select(self.socks, [], [], 0)
|
(r, _, _) = select.select(self.socks, [], [], 0)
|
||||||
for s in r:
|
for s in r:
|
||||||
v = s.recv(4096)
|
v = s.recv(4096)
|
||||||
if not v:
|
if not v:
|
||||||
@ -323,25 +336,25 @@ class Proxy(Handler):
|
|||||||
|
|
||||||
class Mux(Handler):
|
class Mux(Handler):
|
||||||
|
|
||||||
def __init__(self, rsock, wsock):
|
def __init__(self, rfile, wfile):
|
||||||
Handler.__init__(self, [rsock, wsock])
|
Handler.__init__(self, [rfile, wfile])
|
||||||
self.rsock = rsock
|
self.rfile = rfile
|
||||||
self.wsock = wsock
|
self.wfile = wfile
|
||||||
self.new_channel = self.got_dns_req = self.got_routes = None
|
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_udp_open = self.got_udp_data = self.got_udp_close = None
|
||||||
self.got_host_req = self.got_host_list = None
|
self.got_host_req = self.got_host_list = None
|
||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.chani = 0
|
self.chani = 0
|
||||||
self.want = 0
|
self.want = 0
|
||||||
self.inbuf = b''
|
self.inbuf = b('')
|
||||||
self.outbuf = []
|
self.outbuf = []
|
||||||
self.fullness = 0
|
self.fullness = 0
|
||||||
self.too_full = False
|
self.too_full = False
|
||||||
self.send(0, CMD_PING, b'chicken')
|
self.send(0, CMD_PING, b('chicken'))
|
||||||
|
|
||||||
def next_channel(self):
|
def next_channel(self):
|
||||||
# channel 0 is special, so we never allocate it
|
# channel 0 is special, so we never allocate it
|
||||||
for timeout in range(1024):
|
for _ in range(1024):
|
||||||
self.chani += 1
|
self.chani += 1
|
||||||
if self.chani > MAX_CHANNEL:
|
if self.chani > MAX_CHANNEL:
|
||||||
self.chani = 1
|
self.chani = 1
|
||||||
@ -350,14 +363,14 @@ class Mux(Handler):
|
|||||||
|
|
||||||
def amount_queued(self):
|
def amount_queued(self):
|
||||||
total = 0
|
total = 0
|
||||||
for b in self.outbuf:
|
for byte in self.outbuf:
|
||||||
total += len(b)
|
total += len(byte)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def check_fullness(self):
|
def check_fullness(self):
|
||||||
if self.fullness > 32768:
|
if self.fullness > LATENCY_BUFFER_SIZE:
|
||||||
if not self.too_full:
|
if not self.too_full:
|
||||||
self.send(0, CMD_PING, b'rttest')
|
self.send(0, CMD_PING, b('rttest'))
|
||||||
self.too_full = True
|
self.too_full = True
|
||||||
# ob = []
|
# ob = []
|
||||||
# for b in self.outbuf:
|
# for b in self.outbuf:
|
||||||
@ -368,7 +381,8 @@ class Mux(Handler):
|
|||||||
def send(self, channel, cmd, data):
|
def send(self, channel, cmd, data):
|
||||||
assert isinstance(data, bytes)
|
assert isinstance(data, bytes)
|
||||||
assert len(data) <= 65535
|
assert len(data) <= 65535
|
||||||
p = struct.pack('!ccHHH', b'S', b'S', channel, cmd, len(data)) + data
|
p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \
|
||||||
|
+ data
|
||||||
self.outbuf.append(p)
|
self.outbuf.append(p)
|
||||||
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
|
||||||
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
% (channel, cmd_to_name.get(cmd, hex(cmd)),
|
||||||
@ -422,9 +436,9 @@ class Mux(Handler):
|
|||||||
callback(cmd, data)
|
callback(cmd, data)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
self.wsock.setblocking(False)
|
os.set_blocking(self.wfile.fileno(), False)
|
||||||
if self.outbuf and self.outbuf[0]:
|
if self.outbuf and self.outbuf[0]:
|
||||||
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
|
wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0])
|
||||||
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
|
debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
|
||||||
if wrote:
|
if wrote:
|
||||||
self.outbuf[0] = self.outbuf[0][wrote:]
|
self.outbuf[0] = self.outbuf[0][wrote:]
|
||||||
@ -432,16 +446,17 @@ class Mux(Handler):
|
|||||||
self.outbuf[0:1] = []
|
self.outbuf[0:1] = []
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
self.rsock.setblocking(False)
|
os.set_blocking(self.rfile.fileno(), False)
|
||||||
try:
|
try:
|
||||||
b = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
|
_, e = sys.exc_info()[:2]
|
||||||
raise Fatal('other end: %r' % e)
|
raise Fatal('other end: %r' % e)
|
||||||
# log('<<< %r\n' % b)
|
# log('<<< %r\n' % b)
|
||||||
if b == b'': # EOF
|
if read == b(''): # EOF
|
||||||
self.ok = False
|
self.ok = False
|
||||||
if b:
|
if read:
|
||||||
self.inbuf += b
|
self.inbuf += read
|
||||||
|
|
||||||
def handle(self):
|
def handle(self):
|
||||||
self.fill()
|
self.fill()
|
||||||
@ -451,8 +466,8 @@ class Mux(Handler):
|
|||||||
if len(self.inbuf) >= (self.want or HDR_LEN):
|
if len(self.inbuf) >= (self.want or HDR_LEN):
|
||||||
(s1, s2, channel, cmd, datalen) = \
|
(s1, s2, channel, cmd, datalen) = \
|
||||||
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
||||||
assert(s1 == b'S')
|
assert(s1 == b('S'))
|
||||||
assert(s2 == b'S')
|
assert(s2 == b('S'))
|
||||||
self.want = datalen + HDR_LEN
|
self.want = datalen + HDR_LEN
|
||||||
if self.want and len(self.inbuf) >= self.want:
|
if self.want and len(self.inbuf) >= self.want:
|
||||||
data = self.inbuf[HDR_LEN:self.want]
|
data = self.inbuf[HDR_LEN:self.want]
|
||||||
@ -463,22 +478,22 @@ class Mux(Handler):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def pre_select(self, r, w, x):
|
def pre_select(self, r, w, x):
|
||||||
_add(r, self.rsock)
|
_add(r, self.rfile)
|
||||||
if self.outbuf:
|
if self.outbuf:
|
||||||
_add(w, self.wsock)
|
_add(w, self.wfile)
|
||||||
|
|
||||||
def callback(self, sock):
|
def callback(self, sock):
|
||||||
(r, w, x) = select.select([self.rsock], [self.wsock], [], 0)
|
(r, w, _) = select.select([self.rfile], [self.wfile], [], 0)
|
||||||
if self.rsock in r:
|
if self.rfile in r:
|
||||||
self.handle()
|
self.handle()
|
||||||
if self.outbuf and self.wsock in w:
|
if self.outbuf and self.wfile in w:
|
||||||
self.flush()
|
self.flush()
|
||||||
|
|
||||||
|
|
||||||
class MuxWrapper(SockWrapper):
|
class MuxWrapper(SockWrapper):
|
||||||
|
|
||||||
def __init__(self, mux, channel):
|
def __init__(self, mux, channel):
|
||||||
SockWrapper.__init__(self, mux.rsock, mux.wsock)
|
SockWrapper.__init__(self, mux.rfile, mux.wfile)
|
||||||
self.mux = mux
|
self.mux = mux
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.mux.channels[channel] = self.got_packet
|
self.mux.channels[channel] = self.got_packet
|
||||||
@ -493,17 +508,25 @@ class MuxWrapper(SockWrapper):
|
|||||||
return 'SW%r:Mux#%d' % (self.peername, self.channel)
|
return 'SW%r:Mux#%d' % (self.peername, self.channel)
|
||||||
|
|
||||||
def noread(self):
|
def noread(self):
|
||||||
|
if not self.shut_read:
|
||||||
|
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b(''))
|
||||||
|
self.setnoread()
|
||||||
|
|
||||||
|
def setnoread(self):
|
||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
debug2('%r: done reading\n' % self)
|
debug2('%r: done reading\n' % self)
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b'')
|
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
def nowrite(self):
|
def nowrite(self):
|
||||||
|
if not self.shut_write:
|
||||||
|
self.mux.send(self.channel, CMD_TCP_EOF, b(''))
|
||||||
|
self.setnowrite()
|
||||||
|
|
||||||
|
def setnowrite(self):
|
||||||
if not self.shut_write:
|
if not self.shut_write:
|
||||||
debug2('%r: done writing\n' % self)
|
debug2('%r: done writing\n' % self)
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
self.mux.send(self.channel, CMD_TCP_EOF, b'')
|
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
def maybe_close(self):
|
def maybe_close(self):
|
||||||
@ -526,15 +549,17 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def uread(self):
|
def uread(self):
|
||||||
if self.shut_read:
|
if self.shut_read:
|
||||||
return b'' # EOF
|
return b('') # EOF
|
||||||
else:
|
else:
|
||||||
return None # no data available right now
|
return None # no data available right now
|
||||||
|
|
||||||
def got_packet(self, cmd, data):
|
def got_packet(self, cmd, data):
|
||||||
if cmd == CMD_TCP_EOF:
|
if cmd == CMD_TCP_EOF:
|
||||||
self.noread()
|
# Remote side already knows the status - set flag but don't notify
|
||||||
|
self.setnoread()
|
||||||
elif cmd == CMD_TCP_STOP_SENDING:
|
elif cmd == CMD_TCP_STOP_SENDING:
|
||||||
self.nowrite()
|
# Remote side already knows the status - set flag but don't notify
|
||||||
|
self.setnowrite()
|
||||||
elif cmd == CMD_TCP_DATA:
|
elif cmd == CMD_TCP_DATA:
|
||||||
self.buf.append(data)
|
self.buf.append(data)
|
||||||
else:
|
else:
|
||||||
@ -548,7 +573,7 @@ def connect_dst(family, ip, port):
|
|||||||
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
return SockWrapper(outsock, outsock,
|
return SockWrapper(outsock, outsock,
|
||||||
connect_to=(ip, port),
|
connect_to=(ip, port),
|
||||||
peername = '%s:%d' % (ip, port))
|
peername='%s:%d' % (ip, port))
|
||||||
|
|
||||||
|
|
||||||
def runonce(handlers, mux):
|
def runonce(handlers, mux):
|
||||||
|
@ -8,12 +8,24 @@ _p = None
|
|||||||
|
|
||||||
def start_syslog():
|
def start_syslog():
|
||||||
global _p
|
global _p
|
||||||
_p = ssubprocess.Popen(['logger',
|
with open(os.devnull, 'w') as devnull:
|
||||||
'-p', 'daemon.notice',
|
_p = ssubprocess.Popen(
|
||||||
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
|
['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():
|
def stderr_to_syslog():
|
||||||
sys.stdout.flush()
|
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
os.dup2(_p.stdin.fileno(), 2)
|
os.dup2(_p.stdin.fileno(), sys.stderr.fileno())
|
||||||
|
64
sshuttle/sudoers.py
Normal file
64
sshuttle/sudoers.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import getpass
|
||||||
|
from uuid import uuid4
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
from sshuttle.helpers import log, debug1
|
||||||
|
from distutils import spawn
|
||||||
|
|
||||||
|
path_to_sshuttle = sys.argv[0]
|
||||||
|
path_to_dist_packages = os.path.dirname(os.path.abspath(__file__))[:-9]
|
||||||
|
|
||||||
|
# randomize command alias to avoid collisions
|
||||||
|
command_alias = 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()}
|
||||||
|
|
||||||
|
# Template for the sudoers file
|
||||||
|
template = '''
|
||||||
|
Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s *
|
||||||
|
|
||||||
|
%(user_name)s ALL=NOPASSWD: %(ca)s
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def build_config(user_name):
|
||||||
|
content = template % {
|
||||||
|
'ca': command_alias,
|
||||||
|
'dist_packages': path_to_dist_packages,
|
||||||
|
'py': sys.executable,
|
||||||
|
'path': path_to_sshuttle,
|
||||||
|
'user_name': user_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(content, file_name):
|
||||||
|
process = Popen([
|
||||||
|
'/usr/bin/sudo',
|
||||||
|
spawn.find_executable('sudoers-add'),
|
||||||
|
file_name,
|
||||||
|
], stdout=PIPE, stdin=PIPE)
|
||||||
|
|
||||||
|
process.stdin.write(content.encode())
|
||||||
|
|
||||||
|
streamdata = process.communicate()[0]
|
||||||
|
returncode = process.returncode
|
||||||
|
|
||||||
|
if returncode:
|
||||||
|
log('Failed updating sudoers file.\n')
|
||||||
|
debug1(streamdata)
|
||||||
|
exit(returncode)
|
||||||
|
else:
|
||||||
|
log('Success, sudoers file update.\n')
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def sudoers(user_name=None, no_modify=None, file_name=None):
|
||||||
|
user_name = user_name or getpass.getuser()
|
||||||
|
content = build_config(user_name)
|
||||||
|
|
||||||
|
if no_modify:
|
||||||
|
sys.stdout.write(content)
|
||||||
|
exit(0)
|
||||||
|
else:
|
||||||
|
save_config(content, file_name)
|
@ -1,302 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from mock import Mock, patch, call, ANY
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from sshuttle.methods import get_method
|
|
||||||
from sshuttle.helpers import Fatal
|
|
||||||
from sshuttle.methods.pf import OsDefs
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_supported_features():
|
|
||||||
method = get_method('pf')
|
|
||||||
features = method.get_supported_features()
|
|
||||||
assert not features.ipv6
|
|
||||||
assert not features.udp
|
|
||||||
assert features.dns
|
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.helpers.verbose', new=3)
|
|
||||||
def test_get_tcp_dstip():
|
|
||||||
sock = Mock()
|
|
||||||
sock.getpeername.return_value = ("127.0.0.1", 1024)
|
|
||||||
sock.getsockname.return_value = ("127.0.0.2", 1025)
|
|
||||||
sock.family = socket.AF_INET
|
|
||||||
|
|
||||||
firewall = Mock()
|
|
||||||
firewall.pfile.readline.return_value = \
|
|
||||||
b"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n"
|
|
||||||
|
|
||||||
method = get_method('pf')
|
|
||||||
method.set_firewall(firewall)
|
|
||||||
assert method.get_tcp_dstip(sock) == ('127.0.0.3', 1026)
|
|
||||||
|
|
||||||
assert sock.mock_calls == [
|
|
||||||
call.getpeername(),
|
|
||||||
call.getsockname(),
|
|
||||||
]
|
|
||||||
assert firewall.mock_calls == [
|
|
||||||
call.pfile.write(b'QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'),
|
|
||||||
call.pfile.flush(),
|
|
||||||
call.pfile.readline()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_recv_udp():
|
|
||||||
sock = Mock()
|
|
||||||
sock.recvfrom.return_value = "11111", "127.0.0.1"
|
|
||||||
method = get_method('pf')
|
|
||||||
result = method.recv_udp(sock, 1024)
|
|
||||||
assert sock.mock_calls == [call.recvfrom(1024)]
|
|
||||||
assert result == ("127.0.0.1", None, "11111")
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_udp():
|
|
||||||
sock = Mock()
|
|
||||||
method = get_method('pf')
|
|
||||||
method.send_udp(sock, None, "127.0.0.1", "22222")
|
|
||||||
assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")]
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_tcp_listener():
|
|
||||||
listener = Mock()
|
|
||||||
method = get_method('pf')
|
|
||||||
method.setup_tcp_listener(listener)
|
|
||||||
assert listener.mock_calls == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_udp_listener():
|
|
||||||
listener = Mock()
|
|
||||||
method = get_method('pf')
|
|
||||||
method.setup_udp_listener(listener)
|
|
||||||
assert listener.mock_calls == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_assert_features():
|
|
||||||
method = get_method('pf')
|
|
||||||
features = method.get_supported_features()
|
|
||||||
method.assert_features(features)
|
|
||||||
|
|
||||||
features.udp = True
|
|
||||||
with pytest.raises(Fatal):
|
|
||||||
method.assert_features(features)
|
|
||||||
|
|
||||||
features.ipv6 = True
|
|
||||||
with pytest.raises(Fatal):
|
|
||||||
method.assert_features(features)
|
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin'))
|
|
||||||
@patch('sshuttle.methods.pf.sys.stdout')
|
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
|
||||||
def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
|
||||||
method = get_method('pf')
|
|
||||||
assert not method.firewall_command("somthing")
|
|
||||||
|
|
||||||
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
|
||||||
socket.AF_INET, socket.IPPROTO_TCP,
|
|
||||||
"127.0.0.1", 1025, "127.0.0.2", 1024)
|
|
||||||
assert method.firewall_command(command)
|
|
||||||
|
|
||||||
assert mock_pf_get_dev.mock_calls == [call()]
|
|
||||||
assert mock_ioctl.mock_calls == [
|
|
||||||
call(mock_pf_get_dev(), 0xc0544417, ANY),
|
|
||||||
]
|
|
||||||
assert mock_stdout.mock_calls == [
|
|
||||||
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
|
|
||||||
call.flush(),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin'))
|
|
||||||
@patch('sshuttle.methods.pf.sys.stdout')
|
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
|
||||||
def test_firewall_command_notdarwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
|
||||||
method = get_method('pf')
|
|
||||||
assert not method.firewall_command("somthing")
|
|
||||||
|
|
||||||
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
|
||||||
socket.AF_INET, socket.IPPROTO_TCP,
|
|
||||||
"127.0.0.1", 1025, "127.0.0.2", 1024)
|
|
||||||
assert method.firewall_command(command)
|
|
||||||
|
|
||||||
assert mock_pf_get_dev.mock_calls == [call()]
|
|
||||||
assert mock_ioctl.mock_calls == [
|
|
||||||
call(mock_pf_get_dev(), 0xc04c4417, ANY),
|
|
||||||
]
|
|
||||||
assert mock_stdout.mock_calls == [
|
|
||||||
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
|
|
||||||
call.flush(),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def pfctl(args, stdin=None):
|
|
||||||
if args == '-s all':
|
|
||||||
return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n',
|
|
||||||
b'little lamb\n')
|
|
||||||
if args == '-E':
|
|
||||||
return (b'\n', b'Token : abcdefg\n')
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.helpers.verbose', new=3)
|
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin'))
|
|
||||||
@patch('sshuttle.methods.pf.pfctl')
|
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
|
||||||
def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|
||||||
mock_pfctl.side_effect = pfctl
|
|
||||||
|
|
||||||
method = get_method('pf')
|
|
||||||
assert method.name == 'pf'
|
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
|
||||||
method.setup_firewall(
|
|
||||||
1024, 1026,
|
|
||||||
[(10, u'2404:6800:4004:80c::33')],
|
|
||||||
10,
|
|
||||||
[(10, 64, False, u'2404:6800:4004:80c::'),
|
|
||||||
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
|
||||||
True)
|
|
||||||
assert str(excinfo.value) \
|
|
||||||
== 'Address family "AF_INET6" unsupported by pf method_name'
|
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
|
||||||
assert mock_ioctl.mock_calls == []
|
|
||||||
assert mock_pfctl.mock_calls == []
|
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
|
||||||
method.setup_firewall(
|
|
||||||
1025, 1027,
|
|
||||||
[(2, u'1.2.3.33')],
|
|
||||||
2,
|
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
|
||||||
True)
|
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
|
||||||
assert mock_ioctl.mock_calls == []
|
|
||||||
assert mock_pfctl.mock_calls == []
|
|
||||||
|
|
||||||
method.setup_firewall(
|
|
||||||
1025, 1027,
|
|
||||||
[(2, u'1.2.3.33')],
|
|
||||||
2,
|
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
|
||||||
False)
|
|
||||||
assert mock_ioctl.mock_calls == [
|
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
|
||||||
]
|
|
||||||
assert mock_pfctl.mock_calls == [
|
|
||||||
call('-s all'),
|
|
||||||
call('-a sshuttle -f /dev/stdin',
|
|
||||||
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
|
|
||||||
b'table <dns_servers> {1.2.3.33}\n'
|
|
||||||
b'rdr pass on lo0 proto tcp '
|
|
||||||
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
|
|
||||||
b'rdr pass on lo0 proto udp '
|
|
||||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
|
||||||
b'pass out route-to lo0 inet proto tcp '
|
|
||||||
b'to <forward_subnets> keep state\n'
|
|
||||||
b'pass out route-to lo0 inet proto udp '
|
|
||||||
b'to <dns_servers> port 53 keep state\n'),
|
|
||||||
call('-E'),
|
|
||||||
]
|
|
||||||
mock_pf_get_dev.reset_mock()
|
|
||||||
mock_ioctl.reset_mock()
|
|
||||||
mock_pfctl.reset_mock()
|
|
||||||
|
|
||||||
method.restore_firewall(1025, 2, False)
|
|
||||||
assert mock_ioctl.mock_calls == []
|
|
||||||
assert mock_pfctl.mock_calls == [
|
|
||||||
call('-a sshuttle -F all'),
|
|
||||||
call("-X abcdefg"),
|
|
||||||
]
|
|
||||||
mock_pf_get_dev.reset_mock()
|
|
||||||
mock_pfctl.reset_mock()
|
|
||||||
mock_ioctl.reset_mock()
|
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.helpers.verbose', new=3)
|
|
||||||
@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin'))
|
|
||||||
@patch('sshuttle.methods.pf.pfctl')
|
|
||||||
@patch('sshuttle.methods.pf.ioctl')
|
|
||||||
@patch('sshuttle.methods.pf.pf_get_dev')
|
|
||||||
def test_setup_firewall_notdarwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
|
||||||
mock_pfctl.side_effect = pfctl
|
|
||||||
|
|
||||||
method = get_method('pf')
|
|
||||||
assert method.name == 'pf'
|
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
|
||||||
method.setup_firewall(
|
|
||||||
1024, 1026,
|
|
||||||
[(10, u'2404:6800:4004:80c::33')],
|
|
||||||
10,
|
|
||||||
[(10, 64, False, u'2404:6800:4004:80c::'),
|
|
||||||
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
|
||||||
True)
|
|
||||||
assert str(excinfo.value) \
|
|
||||||
== 'Address family "AF_INET6" unsupported by pf method_name'
|
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
|
||||||
assert mock_ioctl.mock_calls == []
|
|
||||||
assert mock_pfctl.mock_calls == []
|
|
||||||
|
|
||||||
with pytest.raises(Exception) as excinfo:
|
|
||||||
method.setup_firewall(
|
|
||||||
1025, 1027,
|
|
||||||
[(2, u'1.2.3.33')],
|
|
||||||
2,
|
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
|
||||||
True)
|
|
||||||
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
|
||||||
assert mock_pf_get_dev.mock_calls == []
|
|
||||||
assert mock_ioctl.mock_calls == []
|
|
||||||
assert mock_pfctl.mock_calls == []
|
|
||||||
|
|
||||||
method.setup_firewall(
|
|
||||||
1025, 1027,
|
|
||||||
[(2, u'1.2.3.33')],
|
|
||||||
2,
|
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
|
||||||
False)
|
|
||||||
assert mock_ioctl.mock_calls == [
|
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
|
||||||
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
|
||||||
]
|
|
||||||
assert mock_pfctl.mock_calls == [
|
|
||||||
call('-s all'),
|
|
||||||
call('-a sshuttle -f /dev/stdin',
|
|
||||||
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
|
|
||||||
b'table <dns_servers> {1.2.3.33}\n'
|
|
||||||
b'rdr pass on lo0 proto tcp '
|
|
||||||
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
|
|
||||||
b'rdr pass on lo0 proto udp '
|
|
||||||
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
|
|
||||||
b'pass out route-to lo0 inet proto tcp '
|
|
||||||
b'to <forward_subnets> keep state\n'
|
|
||||||
b'pass out route-to lo0 inet proto udp '
|
|
||||||
b'to <dns_servers> port 53 keep state\n'),
|
|
||||||
call('-e'),
|
|
||||||
]
|
|
||||||
mock_pf_get_dev.reset_mock()
|
|
||||||
mock_ioctl.reset_mock()
|
|
||||||
mock_pfctl.reset_mock()
|
|
||||||
|
|
||||||
method.restore_firewall(1025, 2, False)
|
|
||||||
assert mock_ioctl.mock_calls == []
|
|
||||||
assert mock_pfctl.mock_calls == [
|
|
||||||
call('-a sshuttle -F all'),
|
|
||||||
call("-d"),
|
|
||||||
]
|
|
||||||
mock_pf_get_dev.reset_mock()
|
|
||||||
mock_pfctl.reset_mock()
|
|
||||||
mock_ioctl.reset_mock()
|
|
@ -1,283 +0,0 @@
|
|||||||
from mock import Mock, patch, call
|
|
||||||
|
|
||||||
from sshuttle.methods import get_method
|
|
||||||
|
|
||||||
|
|
||||||
@patch("sshuttle.methods.tproxy.recvmsg")
|
|
||||||
def test_get_supported_features_recvmsg(mock_recvmsg):
|
|
||||||
method = get_method('tproxy')
|
|
||||||
features = method.get_supported_features()
|
|
||||||
assert features.ipv6
|
|
||||||
assert features.udp
|
|
||||||
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)
|
|
||||||
method = get_method('tproxy')
|
|
||||||
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
|
||||||
assert sock.mock_calls == [call.getsockname()]
|
|
||||||
|
|
||||||
|
|
||||||
@patch("sshuttle.methods.tproxy.recv_udp")
|
|
||||||
def test_recv_udp(mock_recv_udp):
|
|
||||||
mock_recv_udp.return_value = ("127.0.0.1", "127.0.0.2", "11111")
|
|
||||||
|
|
||||||
sock = Mock()
|
|
||||||
method = get_method('tproxy')
|
|
||||||
result = method.recv_udp(sock, 1024)
|
|
||||||
assert sock.mock_calls == []
|
|
||||||
assert mock_recv_udp.mock_calls == [call(sock, 1024)]
|
|
||||||
assert result == ("127.0.0.1", "127.0.0.2", "11111")
|
|
||||||
|
|
||||||
|
|
||||||
@patch("sshuttle.methods.socket.socket")
|
|
||||||
def test_send_udp(mock_socket):
|
|
||||||
sock = Mock()
|
|
||||||
method = get_method('tproxy')
|
|
||||||
method.send_udp(sock, "127.0.0.2", "127.0.0.1", "2222222")
|
|
||||||
assert sock.mock_calls == []
|
|
||||||
assert mock_socket.mock_calls == [
|
|
||||||
call(sock.family, 2),
|
|
||||||
call().setsockopt(1, 2, 1),
|
|
||||||
call().setsockopt(0, 19, 1),
|
|
||||||
call().bind('127.0.0.2'),
|
|
||||||
call().sendto("2222222", '127.0.0.1'),
|
|
||||||
call().close()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_tcp_listener():
|
|
||||||
listener = Mock()
|
|
||||||
method = get_method('tproxy')
|
|
||||||
method.setup_tcp_listener(listener)
|
|
||||||
assert listener.mock_calls == [
|
|
||||||
call.setsockopt(0, 19, 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_udp_listener():
|
|
||||||
listener = Mock()
|
|
||||||
method = get_method('tproxy')
|
|
||||||
method.setup_udp_listener(listener)
|
|
||||||
assert listener.mock_calls == [
|
|
||||||
call.setsockopt(0, 19, 1),
|
|
||||||
call.v4.setsockopt(0, 20, 1),
|
|
||||||
call.v6.setsockopt(41, 74, 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_assert_features():
|
|
||||||
method = get_method('tproxy')
|
|
||||||
features = method.get_supported_features()
|
|
||||||
method.assert_features(features)
|
|
||||||
|
|
||||||
|
|
||||||
def test_firewall_command():
|
|
||||||
method = get_method('tproxy')
|
|
||||||
assert not method.firewall_command("somthing")
|
|
||||||
|
|
||||||
|
|
||||||
@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):
|
|
||||||
mock_ipt_chain_exists.return_value = True
|
|
||||||
method = get_method('tproxy')
|
|
||||||
assert method.name == 'tproxy'
|
|
||||||
|
|
||||||
# IPV6
|
|
||||||
|
|
||||||
method.setup_firewall(
|
|
||||||
1024, 1026,
|
|
||||||
[(10, u'2404:6800:4004:80c::33')],
|
|
||||||
10,
|
|
||||||
[(10, 64, False, u'2404:6800:4004:80c::'),
|
|
||||||
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
|
||||||
True)
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
|
||||||
call(10, 'mangle', 'sshuttle-m-1024'),
|
|
||||||
call(10, 'mangle', 'sshuttle-t-1024'),
|
|
||||||
call(10, 'mangle', 'sshuttle-d-1024')
|
|
||||||
]
|
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
|
||||||
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-m-1024'),
|
|
||||||
call(10, 'mangle', '-X', 'sshuttle-m-1024'),
|
|
||||||
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-t-1024'),
|
|
||||||
call(10, 'mangle', '-X', 'sshuttle-t-1024'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-d-1024'),
|
|
||||||
call(10, 'mangle', '-X', 'sshuttle-d-1024'),
|
|
||||||
call(10, 'mangle', '-N', 'sshuttle-m-1024'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-m-1024'),
|
|
||||||
call(10, 'mangle', '-N', 'sshuttle-d-1024'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-d-1024'),
|
|
||||||
call(10, 'mangle', '-N', 'sshuttle-t-1024'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-t-1024'),
|
|
||||||
call(10, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
|
||||||
call(10, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1024'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
|
|
||||||
'--set-mark', '1'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
|
||||||
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
|
||||||
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
|
||||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
|
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1',
|
|
||||||
'--dest', u'2404:6800:4004:80c::33/32',
|
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
|
||||||
'--dest', u'2404:6800:4004:80c::101f/128',
|
|
||||||
'-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
|
|
||||||
'--dest', u'2404:6800:4004:80c::101f/128',
|
|
||||||
'-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
|
||||||
'--dest', u'2404:6800:4004:80c::101f/128',
|
|
||||||
'-m', 'udp', '-p', 'udp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
|
|
||||||
'--dest', u'2404:6800:4004:80c::101f/128',
|
|
||||||
'-m', 'udp', '-p', 'udp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
|
||||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
|
||||||
'-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
|
||||||
'-m', 'tcp', '-p', 'tcp', '--on-port', '1024'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
|
||||||
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
|
||||||
'-m', 'udp', '-p', 'udp'),
|
|
||||||
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
|
||||||
'-m', 'udp', '-p', 'udp', '--on-port', '1024')
|
|
||||||
]
|
|
||||||
mock_ipt_chain_exists.reset_mock()
|
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
|
||||||
|
|
||||||
method.restore_firewall(1025, 10, True)
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
|
||||||
call(10, 'mangle', 'sshuttle-m-1025'),
|
|
||||||
call(10, 'mangle', 'sshuttle-t-1025'),
|
|
||||||
call(10, 'mangle', 'sshuttle-d-1025')
|
|
||||||
]
|
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
|
||||||
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-m-1025'),
|
|
||||||
call(10, 'mangle', '-X', 'sshuttle-m-1025'),
|
|
||||||
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-t-1025'),
|
|
||||||
call(10, 'mangle', '-X', 'sshuttle-t-1025'),
|
|
||||||
call(10, 'mangle', '-F', 'sshuttle-d-1025'),
|
|
||||||
call(10, 'mangle', '-X', 'sshuttle-d-1025')
|
|
||||||
]
|
|
||||||
mock_ipt_chain_exists.reset_mock()
|
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
|
||||||
|
|
||||||
# IPV4
|
|
||||||
|
|
||||||
method.setup_firewall(
|
|
||||||
1025, 1027,
|
|
||||||
[(2, u'1.2.3.33')],
|
|
||||||
2,
|
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
|
||||||
True)
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
|
||||||
call(2, 'mangle', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', 'sshuttle-d-1025')
|
|
||||||
]
|
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
|
||||||
call(2, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-X', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-X', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
|
|
||||||
call(2, 'mangle', '-X', 'sshuttle-d-1025'),
|
|
||||||
call(2, 'mangle', '-N', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-N', 'sshuttle-d-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
|
|
||||||
call(2, 'mangle', '-N', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-d-1025',
|
|
||||||
'-j', 'MARK', '--set-mark', '1'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
|
||||||
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
|
||||||
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
|
||||||
'--set-mark', '1', '--dest', u'1.2.3.33/32',
|
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
|
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
|
||||||
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
|
|
||||||
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
|
||||||
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
|
|
||||||
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
|
||||||
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
|
||||||
'-m', 'tcp', '-p', 'tcp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
|
|
||||||
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
|
||||||
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
|
||||||
'-m', 'udp', '-p', 'udp'),
|
|
||||||
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1', '--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, 2, True)
|
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
|
||||||
call(2, 'mangle', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', 'sshuttle-d-1025')
|
|
||||||
]
|
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
|
||||||
assert mock_ipt.mock_calls == [
|
|
||||||
call(2, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-X', 'sshuttle-m-1025'),
|
|
||||||
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-X', 'sshuttle-t-1025'),
|
|
||||||
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
|
|
||||||
call(2, 'mangle', '-X', 'sshuttle-d-1025')
|
|
||||||
]
|
|
||||||
mock_ipt_chain_exists.reset_mock()
|
|
||||||
mock_ipt_ttl.reset_mock()
|
|
||||||
mock_ipt.reset_mock()
|
|
@ -1,22 +1,23 @@
|
|||||||
from mock import Mock, patch, call
|
|
||||||
import io
|
import io
|
||||||
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
|
from mock import Mock, patch, call
|
||||||
import sshuttle.firewall
|
import sshuttle.firewall
|
||||||
|
|
||||||
|
|
||||||
def setup_daemon():
|
def setup_daemon():
|
||||||
stdin = io.StringIO(u"""ROUTES
|
stdin = io.StringIO(u"""ROUTES
|
||||||
2,24,0,1.2.3.0
|
{inet},24,0,1.2.3.0,8000,9000
|
||||||
2,32,1,1.2.3.66
|
{inet},32,1,1.2.3.66,8080,8080
|
||||||
10,64,0,2404:6800:4004:80c::
|
{inet6},64,0,2404:6800:4004:80c::,0,0
|
||||||
10,128,1,2404:6800:4004:80c::101f
|
{inet6},128,1,2404:6800:4004:80c::101f,80,80
|
||||||
NSLIST
|
NSLIST
|
||||||
2,1.2.3.33
|
{inet},1.2.3.33
|
||||||
10,2404:6800:4004:80c::33
|
{inet6},2404:6800:4004:80c::33
|
||||||
PORTS 1024,1025,1026,1027
|
PORTS 1024,1025,1026,1027
|
||||||
GO 1
|
GO 1 -
|
||||||
HOST 1.2.3.3,existing
|
HOST 1.2.3.3,existing
|
||||||
""")
|
""".format(inet=AF_INET, inet6=AF_INET6))
|
||||||
stdout = Mock()
|
stdout = Mock()
|
||||||
return stdin, stdout
|
return stdin, stdout
|
||||||
|
|
||||||
@ -58,6 +59,37 @@ def test_rewrite_etc_hosts(tmpdir):
|
|||||||
assert orig_hosts.computehash() == new_hosts.computehash()
|
assert orig_hosts.computehash() == new_hosts.computehash()
|
||||||
|
|
||||||
|
|
||||||
|
def test_subnet_weight():
|
||||||
|
subnets = [
|
||||||
|
(AF_INET, 16, 0, '192.168.0.0', 0, 0),
|
||||||
|
(AF_INET, 24, 0, '192.168.69.0', 0, 0),
|
||||||
|
(AF_INET, 32, 0, '192.168.69.70', 0, 0),
|
||||||
|
(AF_INET, 32, 1, '192.168.69.70', 0, 0),
|
||||||
|
(AF_INET, 32, 1, '192.168.69.70', 80, 80),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 0, 0),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 8000, 9000),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 8000, 8500),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 8000, 8000),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 400, 450)
|
||||||
|
]
|
||||||
|
subnets_sorted = [
|
||||||
|
(AF_INET, 32, 1, '192.168.69.70', 80, 80),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 8000, 8000),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 400, 450),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 8000, 8500),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 8000, 9000),
|
||||||
|
(AF_INET, 32, 1, '192.168.69.70', 0, 0),
|
||||||
|
(AF_INET, 32, 0, '192.168.69.70', 0, 0),
|
||||||
|
(AF_INET, 24, 0, '192.168.69.0', 0, 0),
|
||||||
|
(AF_INET, 16, 0, '192.168.0.0', 0, 0),
|
||||||
|
(AF_INET, 0, 1, '0.0.0.0', 0, 0)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert subnets_sorted == sorted(subnets,
|
||||||
|
key=sshuttle.firewall.subnet_weight,
|
||||||
|
reverse=True)
|
||||||
|
|
||||||
|
|
||||||
@patch('sshuttle.firewall.rewrite_etc_hosts')
|
@patch('sshuttle.firewall.rewrite_etc_hosts')
|
||||||
@patch('sshuttle.firewall.setup_daemon')
|
@patch('sshuttle.firewall.setup_daemon')
|
||||||
@patch('sshuttle.firewall.get_method')
|
@patch('sshuttle.firewall.get_method')
|
||||||
@ -86,17 +118,20 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
|
|||||||
call('not_auto'),
|
call('not_auto'),
|
||||||
call().setup_firewall(
|
call().setup_firewall(
|
||||||
1024, 1026,
|
1024, 1026,
|
||||||
[(10, u'2404:6800:4004:80c::33')],
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
10,
|
AF_INET6,
|
||||||
[(10, 64, False, u'2404:6800:4004:80c::'),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
||||||
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
||||||
True),
|
True,
|
||||||
|
None),
|
||||||
call().setup_firewall(
|
call().setup_firewall(
|
||||||
1025, 1027,
|
1025, 1027,
|
||||||
[(2, u'1.2.3.33')],
|
[(AF_INET, u'1.2.3.33')],
|
||||||
2,
|
AF_INET,
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
True),
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
call().restore_firewall(1024, 10, True),
|
True,
|
||||||
call().restore_firewall(1025, 2, True),
|
None),
|
||||||
|
call().restore_firewall(1024, AF_INET6, True, None),
|
||||||
|
call().restore_firewall(1025, AF_INET, True, None),
|
||||||
]
|
]
|
@ -1,8 +1,9 @@
|
|||||||
from mock import patch, call
|
|
||||||
import sys
|
|
||||||
import io
|
import io
|
||||||
import socket
|
import socket
|
||||||
|
from socket import AF_INET, AF_INET6
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from mock import patch, call
|
||||||
import sshuttle.helpers
|
import sshuttle.helpers
|
||||||
|
|
||||||
|
|
||||||
@ -132,10 +133,12 @@ nameserver 2404:6800:4004:80c::4
|
|||||||
|
|
||||||
ns = sshuttle.helpers.resolvconf_nameservers()
|
ns = sshuttle.helpers.resolvconf_nameservers()
|
||||||
assert ns == [
|
assert ns == [
|
||||||
(2, u'192.168.1.1'), (2, u'192.168.2.1'),
|
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
||||||
(2, u'192.168.3.1'), (2, u'192.168.4.1'),
|
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
||||||
(10, u'2404:6800:4004:80c::1'), (10, u'2404:6800:4004:80c::2'),
|
(AF_INET6, u'2404:6800:4004:80c::1'),
|
||||||
(10, u'2404:6800:4004:80c::3'), (10, u'2404:6800:4004:80c::4')
|
(AF_INET6, u'2404:6800:4004:80c::2'),
|
||||||
|
(AF_INET6, u'2404:6800:4004:80c::3'),
|
||||||
|
(AF_INET6, u'2404:6800:4004:80c::4')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -155,37 +158,39 @@ nameserver 2404:6800:4004:80c::4
|
|||||||
""")
|
""")
|
||||||
ns = sshuttle.helpers.resolvconf_random_nameserver()
|
ns = sshuttle.helpers.resolvconf_random_nameserver()
|
||||||
assert ns in [
|
assert ns in [
|
||||||
(2, u'192.168.1.1'), (2, u'192.168.2.1'),
|
(AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'),
|
||||||
(2, u'192.168.3.1'), (2, u'192.168.4.1'),
|
(AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'),
|
||||||
(10, u'2404:6800:4004:80c::1'), (10, u'2404:6800:4004:80c::2'),
|
(AF_INET6, u'2404:6800:4004:80c::1'),
|
||||||
(10, u'2404:6800:4004:80c::3'), (10, u'2404:6800:4004:80c::4')
|
(AF_INET6, u'2404:6800:4004:80c::2'),
|
||||||
|
(AF_INET6, u'2404:6800:4004:80c::3'),
|
||||||
|
(AF_INET6, u'2404:6800:4004:80c::4')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_islocal():
|
@patch('sshuttle.helpers.socket.socket.bind')
|
||||||
assert sshuttle.helpers.islocal("127.0.0.1", socket.AF_INET)
|
def test_islocal(mock_bind):
|
||||||
assert not sshuttle.helpers.islocal("192.0.2.1", socket.AF_INET)
|
bind_error = socket.error(errno.EADDRNOTAVAIL)
|
||||||
assert sshuttle.helpers.islocal("::1", socket.AF_INET6)
|
mock_bind.side_effect = [None, bind_error, None, bind_error]
|
||||||
assert not sshuttle.helpers.islocal("2001:db8::1", socket.AF_INET6)
|
|
||||||
|
assert sshuttle.helpers.islocal("127.0.0.1", AF_INET)
|
||||||
|
assert not sshuttle.helpers.islocal("192.0.2.1", AF_INET)
|
||||||
|
assert sshuttle.helpers.islocal("::1", AF_INET6)
|
||||||
|
assert not sshuttle.helpers.islocal("2001:db8::1", AF_INET6)
|
||||||
|
|
||||||
|
|
||||||
def test_family_ip_tuple():
|
def test_family_ip_tuple():
|
||||||
assert sshuttle.helpers.family_ip_tuple("127.0.0.1") \
|
assert sshuttle.helpers.family_ip_tuple("127.0.0.1") \
|
||||||
== (socket.AF_INET, "127.0.0.1")
|
== (AF_INET, "127.0.0.1")
|
||||||
assert sshuttle.helpers.family_ip_tuple("192.168.2.6") \
|
assert sshuttle.helpers.family_ip_tuple("192.168.2.6") \
|
||||||
== (socket.AF_INET, "192.168.2.6")
|
== (AF_INET, "192.168.2.6")
|
||||||
assert sshuttle.helpers.family_ip_tuple("::1") \
|
assert sshuttle.helpers.family_ip_tuple("::1") \
|
||||||
== (socket.AF_INET6, "::1")
|
== (AF_INET6, "::1")
|
||||||
assert sshuttle.helpers.family_ip_tuple("2404:6800:4004:80c::1") \
|
assert sshuttle.helpers.family_ip_tuple("2404:6800:4004:80c::1") \
|
||||||
== (socket.AF_INET6, "2404:6800:4004:80c::1")
|
== (AF_INET6, "2404:6800:4004:80c::1")
|
||||||
|
|
||||||
|
|
||||||
def test_family_to_string():
|
def test_family_to_string():
|
||||||
assert sshuttle.helpers.family_to_string(socket.AF_INET) == "AF_INET"
|
assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET"
|
||||||
assert sshuttle.helpers.family_to_string(socket.AF_INET6) == "AF_INET6"
|
assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6"
|
||||||
if sys.version_info < (3, 0):
|
expected = 'AddressFamily.AF_UNIX'
|
||||||
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
|
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected
|
@ -1,8 +1,9 @@
|
|||||||
import pytest
|
|
||||||
from mock import Mock, patch, call
|
|
||||||
import socket
|
import socket
|
||||||
|
from socket import AF_INET, AF_INET6
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mock import Mock, patch, call
|
||||||
from sshuttle.helpers import Fatal
|
from sshuttle.helpers import Fatal
|
||||||
from sshuttle.methods import get_method
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ def test_get_supported_features():
|
|||||||
def test_get_tcp_dstip():
|
def test_get_tcp_dstip():
|
||||||
sock = Mock()
|
sock = Mock()
|
||||||
sock.getsockopt.return_value = struct.pack(
|
sock.getsockopt.return_value = struct.pack(
|
||||||
'!HHBBBB', socket.ntohs(socket.AF_INET), 1024, 127, 0, 0, 1)
|
'!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1)
|
||||||
method = get_method('nat')
|
method = get_method('nat')
|
||||||
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
||||||
assert sock.mock_calls == [call.getsockopt(0, 80, 16)]
|
assert sock.mock_calls == [call.getsockopt(0, 80, 16)]
|
||||||
@ -84,11 +85,12 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
with pytest.raises(Exception) as excinfo:
|
with pytest.raises(Exception) as excinfo:
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
1024, 1026,
|
1024, 1026,
|
||||||
[(10, u'2404:6800:4004:80c::33')],
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
10,
|
AF_INET6,
|
||||||
[(10, 64, False, u'2404:6800:4004:80c::'),
|
[(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
|
||||||
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
(AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
|
||||||
True)
|
True,
|
||||||
|
None)
|
||||||
assert str(excinfo.value) \
|
assert str(excinfo.value) \
|
||||||
== 'Address family "AF_INET6" unsupported by nat method_name'
|
== 'Address family "AF_INET6" unsupported by nat method_name'
|
||||||
assert mock_ipt_chain_exists.mock_calls == []
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
@ -98,10 +100,12 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
with pytest.raises(Exception) as excinfo:
|
with pytest.raises(Exception) as excinfo:
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
1025, 1027,
|
1025, 1027,
|
||||||
[(2, u'1.2.3.33')],
|
[(AF_INET, u'1.2.3.33')],
|
||||||
2,
|
AF_INET,
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
True)
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
|
True,
|
||||||
|
None)
|
||||||
assert str(excinfo.value) == 'UDP not supported by nat method_name'
|
assert str(excinfo.value) == 'UDP not supported by nat method_name'
|
||||||
assert mock_ipt_chain_exists.mock_calls == []
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
@ -109,46 +113,55 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
|||||||
|
|
||||||
method.setup_firewall(
|
method.setup_firewall(
|
||||||
1025, 1027,
|
1025, 1027,
|
||||||
[(2, u'1.2.3.33')],
|
[(AF_INET, u'1.2.3.33')],
|
||||||
2,
|
AF_INET,
|
||||||
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
[(AF_INET, 24, False, u'1.2.3.0', 8000, 9000),
|
||||||
False)
|
(AF_INET, 32, True, u'1.2.3.66', 8080, 8080)],
|
||||||
|
False,
|
||||||
|
None)
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(2, 'nat', 'sshuttle-1025')
|
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == [
|
assert mock_ipt_ttl.mock_calls == [
|
||||||
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||||
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--to-ports', '1025'),
|
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
|
||||||
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
'--to-ports', '1025'),
|
||||||
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||||
'--dest', u'1.2.3.33/32', '-p', 'udp',
|
'--dest', u'1.2.3.33/32', '-p', 'udp',
|
||||||
'--dport', '53', '--to-ports', '1027')
|
'--dport', '53', '--to-ports', '1027')
|
||||||
]
|
]
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(2, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-F', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-X', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-X', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-N', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-N', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-F', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
'--dest', u'1.2.3.66/32', '-p', 'tcp')
|
'-m', 'addrtype', '--dst-type', 'LOCAL',
|
||||||
|
'!', '-p', 'udp'),
|
||||||
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
|
'-m', 'addrtype', '--dst-type', 'LOCAL',
|
||||||
|
'-p', 'udp', '!', '--dport', '53'),
|
||||||
|
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
mock_ipt_ttl.reset_mock()
|
||||||
mock_ipt.reset_mock()
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
method.restore_firewall(1025, 2, False)
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
assert mock_ipt_chain_exists.mock_calls == [
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
call(2, 'nat', 'sshuttle-1025')
|
call(AF_INET, 'nat', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
assert mock_ipt_ttl.mock_calls == []
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
assert mock_ipt.mock_calls == [
|
assert mock_ipt.mock_calls == [
|
||||||
call(2, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-F', 'sshuttle-1025'),
|
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
|
||||||
call(2, 'nat', '-X', 'sshuttle-1025')
|
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
|
||||||
]
|
]
|
||||||
mock_ipt_chain_exists.reset_mock()
|
mock_ipt_chain_exists.reset_mock()
|
||||||
mock_ipt_ttl.reset_mock()
|
mock_ipt_ttl.reset_mock()
|
487
tests/client/test_methods_pf.py
Normal file
487
tests/client/test_methods_pf.py
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
import socket
|
||||||
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mock import Mock, patch, call, ANY
|
||||||
|
from sshuttle.methods import get_method
|
||||||
|
from sshuttle.helpers import Fatal
|
||||||
|
from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_supported_features():
|
||||||
|
method = get_method('pf')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
assert features.ipv6
|
||||||
|
assert not features.udp
|
||||||
|
assert features.dns
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
|
def test_get_tcp_dstip():
|
||||||
|
sock = Mock()
|
||||||
|
sock.getpeername.return_value = ("127.0.0.1", 1024)
|
||||||
|
sock.getsockname.return_value = ("127.0.0.2", 1025)
|
||||||
|
sock.family = AF_INET
|
||||||
|
|
||||||
|
firewall = Mock()
|
||||||
|
firewall.pfile.readline.return_value = \
|
||||||
|
b"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n"
|
||||||
|
|
||||||
|
method = get_method('pf')
|
||||||
|
method.set_firewall(firewall)
|
||||||
|
assert method.get_tcp_dstip(sock) == ('127.0.0.3', 1026)
|
||||||
|
|
||||||
|
assert sock.mock_calls == [
|
||||||
|
call.getpeername(),
|
||||||
|
call.getsockname(),
|
||||||
|
]
|
||||||
|
assert firewall.mock_calls == [
|
||||||
|
call.pfile.write(b'QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'),
|
||||||
|
call.pfile.flush(),
|
||||||
|
call.pfile.readline()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_recv_udp():
|
||||||
|
sock = Mock()
|
||||||
|
sock.recvfrom.return_value = "11111", "127.0.0.1"
|
||||||
|
method = get_method('pf')
|
||||||
|
result = method.recv_udp(sock, 1024)
|
||||||
|
assert sock.mock_calls == [call.recvfrom(1024)]
|
||||||
|
assert result == ("127.0.0.1", None, "11111")
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_udp():
|
||||||
|
sock = Mock()
|
||||||
|
method = get_method('pf')
|
||||||
|
method.send_udp(sock, None, "127.0.0.1", "22222")
|
||||||
|
assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_tcp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('pf')
|
||||||
|
method.setup_tcp_listener(listener)
|
||||||
|
assert listener.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_udp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('pf')
|
||||||
|
method.setup_udp_listener(listener)
|
||||||
|
assert listener.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_assert_features():
|
||||||
|
method = get_method('pf')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
method.assert_features(features)
|
||||||
|
|
||||||
|
features.udp = True
|
||||||
|
with pytest.raises(Fatal):
|
||||||
|
method.assert_features(features)
|
||||||
|
|
||||||
|
features.ipv6 = True
|
||||||
|
with pytest.raises(Fatal):
|
||||||
|
method.assert_features(features)
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.methods.pf.pf', Darwin())
|
||||||
|
@patch('sshuttle.methods.pf.sys.stdout')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
|
method = get_method('pf')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
|
AF_INET, socket.IPPROTO_TCP,
|
||||||
|
"127.0.0.1", 1025, "127.0.0.2", 1024)
|
||||||
|
assert method.firewall_command(command)
|
||||||
|
|
||||||
|
assert mock_pf_get_dev.mock_calls == [call()]
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xc0544417, ANY),
|
||||||
|
]
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.methods.pf.pf', FreeBsd())
|
||||||
|
@patch('sshuttle.methods.pf.sys.stdout')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
|
method = get_method('pf')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
|
AF_INET, socket.IPPROTO_TCP,
|
||||||
|
"127.0.0.1", 1025, "127.0.0.2", 1024)
|
||||||
|
assert method.firewall_command(command)
|
||||||
|
|
||||||
|
assert mock_pf_get_dev.mock_calls == [call()]
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xc04c4417, ANY),
|
||||||
|
]
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.methods.pf.pf', OpenBsd())
|
||||||
|
@patch('sshuttle.methods.pf.sys.stdout')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
|
method = get_method('pf')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
|
AF_INET, socket.IPPROTO_TCP,
|
||||||
|
"127.0.0.1", 1025, "127.0.0.2", 1024)
|
||||||
|
assert method.firewall_command(command)
|
||||||
|
|
||||||
|
assert mock_pf_get_dev.mock_calls == [call()]
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xc0504417, ANY),
|
||||||
|
]
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def pfctl(args, stdin=None):
|
||||||
|
if args == '-s Interfaces -i lo -v':
|
||||||
|
return (b'lo0 (skip)',)
|
||||||
|
if args == '-s all':
|
||||||
|
return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n',
|
||||||
|
b'little lamb\n')
|
||||||
|
if args == '-E':
|
||||||
|
return (b'\n', b'Token : abcdefg\n')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
|
@patch('sshuttle.methods.pf.pf', Darwin())
|
||||||
|
@patch('sshuttle.methods.pf.pfctl')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||||
|
mock_pfctl.side_effect = pfctl
|
||||||
|
|
||||||
|
method = get_method('pf')
|
||||||
|
assert method.name == 'pf'
|
||||||
|
|
||||||
|
# IPV6
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
|
AF_INET6,
|
||||||
|
[(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)
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
]
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s Interfaces -i lo -v'),
|
||||||
|
call('-f /dev/stdin', b'pass on lo\n'),
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle6-1024 -f /dev/stdin',
|
||||||
|
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
|
||||||
|
b'rdr pass on lo0 inet6 proto tcp from ! ::1 to '
|
||||||
|
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 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'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(AF_INET, u'1.2.3.33')],
|
||||||
|
AF_INET,
|
||||||
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
|
True,
|
||||||
|
None)
|
||||||
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == []
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(AF_INET, u'1.2.3.33')],
|
||||||
|
AF_INET,
|
||||||
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
|
False,
|
||||||
|
None)
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCC20441A, ANY),
|
||||||
|
]
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s Interfaces -i lo -v'),
|
||||||
|
call('-f /dev/stdin', b'pass on lo\n'),
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle-1025 -f /dev/stdin',
|
||||||
|
b'table <dns_servers> {1.2.3.33}\n'
|
||||||
|
b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 to 1.2.3.0/24 '
|
||||||
|
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 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'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-a sshuttle-1025 -F all'),
|
||||||
|
call("-X abcdefg"),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
|
@patch('sshuttle.methods.pf.pf', FreeBsd())
|
||||||
|
@patch('subprocess.call')
|
||||||
|
@patch('sshuttle.methods.pf.pfctl')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
|
||||||
|
mock_subprocess_call):
|
||||||
|
mock_pfctl.side_effect = pfctl
|
||||||
|
|
||||||
|
method = get_method('pf')
|
||||||
|
assert method.name == 'pf'
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
|
AF_INET6,
|
||||||
|
[(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)
|
||||||
|
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle6-1024 -f /dev/stdin',
|
||||||
|
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
|
||||||
|
b'rdr pass on lo0 inet6 proto tcp from ! ::1 to '
|
||||||
|
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 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
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(AF_INET, u'1.2.3.33')],
|
||||||
|
AF_INET,
|
||||||
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
|
True,
|
||||||
|
None)
|
||||||
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == []
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(AF_INET, u'1.2.3.33')],
|
||||||
|
AF_INET,
|
||||||
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
|
False,
|
||||||
|
None)
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xC4704433, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
||||||
|
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
|
||||||
|
]
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle-1025 -f /dev/stdin',
|
||||||
|
b'table <dns_servers> {1.2.3.33}\n'
|
||||||
|
b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 '
|
||||||
|
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 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'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
|
method.restore_firewall(1024, AF_INET6, False, None)
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-a sshuttle-1025 -F all'),
|
||||||
|
call('-a sshuttle6-1024 -F all'),
|
||||||
|
call("-d"),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
|
@patch('sshuttle.methods.pf.pf', OpenBsd())
|
||||||
|
@patch('sshuttle.methods.pf.pfctl')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||||
|
mock_pfctl.side_effect = pfctl
|
||||||
|
|
||||||
|
method = get_method('pf')
|
||||||
|
assert method.name == 'pf'
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
|
AF_INET6,
|
||||||
|
[(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)
|
||||||
|
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
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'),
|
||||||
|
call('-f /dev/stdin', b'match on lo\n'),
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle6-1024 -f /dev/stdin',
|
||||||
|
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
|
||||||
|
b'pass in on lo0 inet6 proto tcp to 2404:6800:4004:80c::/64 '
|
||||||
|
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 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'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(AF_INET, u'1.2.3.33')],
|
||||||
|
AF_INET,
|
||||||
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
|
True,
|
||||||
|
None)
|
||||||
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == []
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(AF_INET, u'1.2.3.33')],
|
||||||
|
AF_INET,
|
||||||
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
|
False,
|
||||||
|
None)
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
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'),
|
||||||
|
call('-f /dev/stdin', b'match on lo\n'),
|
||||||
|
call('-s all'),
|
||||||
|
call('-a sshuttle-1025 -f /dev/stdin',
|
||||||
|
b'table <dns_servers> {1.2.3.33}\n'
|
||||||
|
b'pass in on lo0 inet proto tcp to 1.2.3.0/24 divert-to '
|
||||||
|
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 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'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
method.restore_firewall(1025, AF_INET, False, None)
|
||||||
|
method.restore_firewall(1024, AF_INET6, False, None)
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == [
|
||||||
|
call('-a sshuttle-1025 -F all'),
|
||||||
|
call('-a sshuttle6-1024 -F all'),
|
||||||
|
call('-d'),
|
||||||
|
]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
297
tests/client/test_methods_tproxy.py
Normal file
297
tests/client/test_methods_tproxy.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import socket
|
||||||
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
|
from mock import Mock, patch, call
|
||||||
|
|
||||||
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
|
|
||||||
|
@patch("sshuttle.methods.tproxy.recvmsg")
|
||||||
|
def test_get_supported_features_recvmsg(mock_recvmsg):
|
||||||
|
method = get_method('tproxy')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
assert features.ipv6
|
||||||
|
assert features.udp
|
||||||
|
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)
|
||||||
|
method = get_method('tproxy')
|
||||||
|
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
||||||
|
assert sock.mock_calls == [call.getsockname()]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("sshuttle.methods.tproxy.recv_udp")
|
||||||
|
def test_recv_udp(mock_recv_udp):
|
||||||
|
mock_recv_udp.return_value = ("127.0.0.1", "127.0.0.2", "11111")
|
||||||
|
|
||||||
|
sock = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
result = method.recv_udp(sock, 1024)
|
||||||
|
assert sock.mock_calls == []
|
||||||
|
assert mock_recv_udp.mock_calls == [call(sock, 1024)]
|
||||||
|
assert result == ("127.0.0.1", "127.0.0.2", "11111")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("sshuttle.methods.socket.socket")
|
||||||
|
def test_send_udp(mock_socket):
|
||||||
|
sock = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
method.send_udp(sock, "127.0.0.2", "127.0.0.1", "2222222")
|
||||||
|
assert sock.mock_calls == []
|
||||||
|
assert mock_socket.mock_calls == [
|
||||||
|
call(sock.family, 2),
|
||||||
|
call().setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),
|
||||||
|
call().setsockopt(0, 19, 1),
|
||||||
|
call().bind('127.0.0.2'),
|
||||||
|
call().sendto("2222222", '127.0.0.1'),
|
||||||
|
call().close()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_tcp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
method.setup_tcp_listener(listener)
|
||||||
|
assert listener.mock_calls == [
|
||||||
|
call.setsockopt(0, 19, 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_udp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
method.setup_udp_listener(listener)
|
||||||
|
assert listener.mock_calls == [
|
||||||
|
call.setsockopt(0, 19, 1),
|
||||||
|
call.v4.setsockopt(0, 20, 1),
|
||||||
|
call.v6.setsockopt(41, 74, 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_assert_features():
|
||||||
|
method = get_method('tproxy')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
method.assert_features(features)
|
||||||
|
|
||||||
|
|
||||||
|
def test_firewall_command():
|
||||||
|
method = get_method('tproxy')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
|
||||||
|
@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):
|
||||||
|
mock_ipt_chain_exists.return_value = True
|
||||||
|
method = get_method('tproxy')
|
||||||
|
assert method.name == 'tproxy'
|
||||||
|
|
||||||
|
# IPV6
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(AF_INET6, u'2404:6800:4004:80c::33')],
|
||||||
|
AF_INET6,
|
||||||
|
[(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)
|
||||||
|
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'),
|
||||||
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-N', 'sshuttle-m-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-N', 'sshuttle-d-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-N', 'sshuttle-t-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||||
|
'sshuttle-t-1024'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
|
||||||
|
'--set-mark', '1'),
|
||||||
|
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',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1',
|
||||||
|
'--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',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
|
'-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
|
'-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
|
||||||
|
'--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',
|
||||||
|
'-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',
|
||||||
|
'-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', '--dport', '8000:9000'),
|
||||||
|
call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
|
||||||
|
'--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)
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(AF_INET6, 'mangle', 'sshuttle-m-1025'),
|
||||||
|
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'),
|
||||||
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1025'),
|
||||||
|
call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
|
# IPV4
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(AF_INET, u'1.2.3.33')],
|
||||||
|
AF_INET,
|
||||||
|
[(AF_INET, 24, False, u'1.2.3.0', 0, 0),
|
||||||
|
(AF_INET, 32, True, u'1.2.3.66', 80, 80)],
|
||||||
|
True,
|
||||||
|
None)
|
||||||
|
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'),
|
||||||
|
call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-N', 'sshuttle-m-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-N', 'sshuttle-d-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-N', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
|
||||||
|
'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
|
||||||
|
'-j', 'MARK', '--set-mark', '1'),
|
||||||
|
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',
|
||||||
|
'-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',
|
||||||
|
'-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',
|
||||||
|
'--dport', '80:80'),
|
||||||
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
|
||||||
|
'--dport', '80:80'),
|
||||||
|
call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
|
||||||
|
'--dport', '80:80'),
|
||||||
|
call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
|
||||||
|
'--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',
|
||||||
|
'-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',
|
||||||
|
'-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',
|
||||||
|
'-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',
|
||||||
|
'-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)
|
||||||
|
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'),
|
||||||
|
call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
101
tests/client/test_options.py
Normal file
101
tests/client/test_options.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import socket
|
||||||
|
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',
|
||||||
|
'10.0': '10.0.0.0',
|
||||||
|
'184.172.10.74': '184.172.10.74',
|
||||||
|
'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)
|
||||||
|
|
||||||
|
_ip6_reprs = {
|
||||||
|
'::': '::',
|
||||||
|
'::1': '::1',
|
||||||
|
'fc00::': 'fc00::',
|
||||||
|
'2a01:7e00:e000:188::1': '2a01:7e00:e000:188::1'
|
||||||
|
}
|
||||||
|
|
||||||
|
_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)
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subnetport_ip4_with_mask():
|
||||||
|
for ip_repr, ip in _ip4_reprs.items():
|
||||||
|
for swidth in _ip4_swidths:
|
||||||
|
assert sshuttle.options.parse_subnetport(
|
||||||
|
'/'.join((ip_repr, str(swidth)))
|
||||||
|
) == (socket.AF_INET, ip, swidth, 0, 0)
|
||||||
|
assert sshuttle.options.parse_subnetport('0/0') \
|
||||||
|
== (socket.AF_INET, '0.0.0.0', 0, 0, 0)
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '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)
|
||||||
|
assert sshuttle.options.parse_subnetport(ip_repr + '/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)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subnetport_ip6_with_mask():
|
||||||
|
for ip_repr, ip in _ip6_reprs.items():
|
||||||
|
for swidth in _ip4_swidths + _ip6_swidths:
|
||||||
|
assert sshuttle.options.parse_subnetport(
|
||||||
|
'/'.join((ip_repr, str(swidth)))
|
||||||
|
) == (socket.AF_INET6, ip, swidth, 0, 0)
|
||||||
|
assert sshuttle.options.parse_subnetport('::/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'
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']: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)
|
||||||
|
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\
|
||||||
|
== (socket.AF_INET6, ip, 16, 80, 90)
|
65
tests/client/test_sdnotify.py
Normal file
65
tests/client/test_sdnotify.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import socket
|
||||||
|
|
||||||
|
from mock import Mock, patch, call
|
||||||
|
|
||||||
|
import sshuttle.sdnotify
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.sdnotify.os.environ.get')
|
||||||
|
def test_notify_invalid_socket_path(mock_get):
|
||||||
|
mock_get.return_value = 'invalid_path'
|
||||||
|
assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready())
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.sdnotify.os.environ.get')
|
||||||
|
def test_notify_socket_not_there(mock_get):
|
||||||
|
mock_get.return_value = '/run/valid_nonexistent_path'
|
||||||
|
assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready())
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.sdnotify.os.environ.get')
|
||||||
|
def test_notify_no_message(mock_get):
|
||||||
|
mock_get.return_value = '/run/valid_path'
|
||||||
|
assert not sshuttle.sdnotify.send()
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.sdnotify.socket.socket')
|
||||||
|
@patch('sshuttle.sdnotify.os.environ.get')
|
||||||
|
def test_notify_socket_error(mock_get, mock_socket):
|
||||||
|
mock_get.return_value = '/run/valid_path'
|
||||||
|
mock_socket.side_effect = socket.error('test error')
|
||||||
|
assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready())
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.sdnotify.socket.socket')
|
||||||
|
@patch('sshuttle.sdnotify.os.environ.get')
|
||||||
|
def test_notify_sendto_error(mock_get, mock_socket):
|
||||||
|
message = sshuttle.sdnotify.ready()
|
||||||
|
socket_path = '/run/valid_path'
|
||||||
|
|
||||||
|
sock = Mock()
|
||||||
|
sock.sendto.side_effect = socket.error('test error')
|
||||||
|
mock_get.return_value = '/run/valid_path'
|
||||||
|
mock_socket.return_value = sock
|
||||||
|
|
||||||
|
assert not sshuttle.sdnotify.send(message)
|
||||||
|
assert sock.sendto.mock_calls == [
|
||||||
|
call(message, socket_path),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.sdnotify.socket.socket')
|
||||||
|
@patch('sshuttle.sdnotify.os.environ.get')
|
||||||
|
def test_notify(mock_get, mock_socket):
|
||||||
|
messages = [sshuttle.sdnotify.ready(), sshuttle.sdnotify.status('Running')]
|
||||||
|
socket_path = '/run/valid_path'
|
||||||
|
|
||||||
|
sock = Mock()
|
||||||
|
sock.sendto.return_value = 1
|
||||||
|
mock_get.return_value = '/run/valid_path'
|
||||||
|
mock_socket.return_value = sock
|
||||||
|
|
||||||
|
assert sshuttle.sdnotify.send(*messages)
|
||||||
|
assert sock.sendto.mock_calls == [
|
||||||
|
call(b'\n'.join(messages), socket_path),
|
||||||
|
]
|
60
tests/server/test_server.py
Normal file
60
tests/server/test_server.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import io
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from mock import patch, Mock
|
||||||
|
|
||||||
|
import sshuttle.server
|
||||||
|
|
||||||
|
|
||||||
|
def test__ipmatch():
|
||||||
|
assert sshuttle.server._ipmatch("1.2.3.4") is not None
|
||||||
|
assert sshuttle.server._ipmatch("::1") is None # ipv6 not supported
|
||||||
|
assert sshuttle.server._ipmatch("42 Example Street, Melbourne") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test__ipstr():
|
||||||
|
assert sshuttle.server._ipstr("1.2.3.4", 24) == "1.2.3.4/24"
|
||||||
|
assert sshuttle.server._ipstr("1.2.3.4", 32) == "1.2.3.4"
|
||||||
|
|
||||||
|
|
||||||
|
def test__maskbits():
|
||||||
|
netmask = sshuttle.server._ipmatch("255.255.255.0")
|
||||||
|
sshuttle.server._maskbits(netmask)
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.server.which', side_effect=lambda x: x == 'netstat')
|
||||||
|
@patch('sshuttle.server.ssubprocess.Popen')
|
||||||
|
def test_listroutes_netstat(mock_popen, mock_which):
|
||||||
|
mock_pobj = Mock()
|
||||||
|
mock_pobj.stdout = io.BytesIO(b"""
|
||||||
|
Kernel IP routing table
|
||||||
|
Destination Gateway Genmask Flags MSS Window irtt Iface
|
||||||
|
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 wlan0
|
||||||
|
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 wlan0
|
||||||
|
""")
|
||||||
|
mock_pobj.wait.return_value = 0
|
||||||
|
mock_popen.return_value = mock_pobj
|
||||||
|
|
||||||
|
routes = sshuttle.server.list_routes()
|
||||||
|
|
||||||
|
assert list(routes) == [
|
||||||
|
(socket.AF_INET, '192.168.1.0', 24)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.server.which', side_effect=lambda x: x == 'ip')
|
||||||
|
@patch('sshuttle.server.ssubprocess.Popen')
|
||||||
|
def test_listroutes_iproute(mock_popen, mock_which):
|
||||||
|
mock_pobj = Mock()
|
||||||
|
mock_pobj.stdout = io.BytesIO(b"""
|
||||||
|
default via 192.168.1.1 dev wlan0 proto static
|
||||||
|
192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.1
|
||||||
|
""")
|
||||||
|
mock_pobj.wait.return_value = 0
|
||||||
|
mock_popen.return_value = mock_pobj
|
||||||
|
|
||||||
|
routes = sshuttle.server.list_routes()
|
||||||
|
|
||||||
|
assert list(routes) == [
|
||||||
|
(socket.AF_INET, '192.168.1.0', 24)
|
||||||
|
]
|
20
tox.ini
20
tox.ini
@ -1,16 +1,22 @@
|
|||||||
[tox]
|
[tox]
|
||||||
downloadcache = {toxworkdir}/cache/
|
downloadcache = {toxworkdir}/cache/
|
||||||
envlist =
|
envlist =
|
||||||
py27,
|
|
||||||
py35,
|
py35,
|
||||||
|
py36,
|
||||||
|
py37,
|
||||||
|
py38,
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
basepython =
|
basepython =
|
||||||
py27: python2.7
|
py36: python3.6
|
||||||
py35: python3.5
|
py37: python3.7
|
||||||
|
py38: python3.8
|
||||||
commands =
|
commands =
|
||||||
py.test
|
pip install -e .
|
||||||
deps =
|
# actual flake8 test
|
||||||
|
flake8 sshuttle tests
|
||||||
|
# flake8 complexity warnings
|
||||||
|
flake8 sshuttle tests --exit-zero --max-complexity=10
|
||||||
pytest
|
pytest
|
||||||
mock
|
deps =
|
||||||
setuptools>=17.1
|
-rrequirements-tests.txt
|
||||||
|
Reference in New Issue
Block a user