Compare commits

...

110 Commits

Author SHA1 Message Date
6ec42adbf4 Prepare for 0.78.4 2018-04-02 14:52:22 +10:00
2200d824bf Improve formatting 2018-03-22 07:59:10 +11:00
9715a1d6f2 Preserve peer and port properly 2018-03-22 07:59:10 +11:00
8bfc03b256 Make --to-dns and --ns-host work well together 2018-03-22 07:59:10 +11:00
884bd6deb0 Remove test that fails under OSX
Fixes #213
2018-03-16 18:40:32 +11:00
a215f1b227 Remove Python 2.6 from automatic tests
Automatic python 2.6 testing is becoming harder, especially as pytest
3.4.2 is unavailable for Python 2.6.
2018-03-16 18:34:15 +11:00
11455d0bcd Various updates to tests 2018-03-16 18:27:50 +11:00
74acc10385 Add entries to .gitignore 2018-03-16 18:10:09 +11:00
084bf5f0f2 Specify pip requirements for tests 2018-03-16 18:10:09 +11:00
1940b524f1 Add nat-like method using nftables instead of iptables 2018-03-13 07:36:00 +11:00
d11f5b9d16 Use flake8 to find Python syntax errors or undefined names 2018-02-22 18:02:36 +11:00
93b969a049 Fix compatibility with the sudoers file
Starting sshuttle without having to type in one's password requires to
put the sudo-ed command in the `/etc/sudoers` file. However, sshuttle
sets an environment variable, which cannot be done as-is in the sudoers
file. This fix prepend the /usr/bin/env command, which allows one to
pass fixed environment variables to a sudo-ed command.

In practice, the sub-command:

```
sudo PYTHONPATH=/usr/lib/python3/dist-packages -- \
        /usr/bin/python3 /usr/bin/sshuttle --method auto --firewall
```

becomes

```
sudo /usr/bin/env PYTHONPATH=/usr/lib/python3/dist-packages \
        /usr/bin/python3 /usr/bin/sshuttle --method auto --firewall
```
2018-02-16 08:07:02 +11:00
f27b27b0e8 Stop using SO_REUSEADDR on sockets 2018-02-16 08:04:22 +11:00
fc08fb4086 Declare 'verbosity' as global variable to placate linters 2018-02-15 21:34:05 +11:00
e82d5a8e7c Adds 'cd sshuttle' after 'git' to README and docs 2018-02-15 07:37:15 +11:00
d9d61e6ab2 Documentation for loading options from configuration file 2018-01-30 17:08:30 +11:00
179bb107e1 Load options from a file
This small change will allow a file path to be passed as argument from which
the command line options will be loaded.

Extra command line options can be passed (in addition to those already in the
file) and existing ones can be overriden.

Example sshuttle.conf file:
192.168.0.0/16
--remote
user@example.com

Example sshuttle call:
sshuttle @/path/to/sshuttle.conf

Example sshuttle call with verbose flags added:
sshuttle @/path/to/sshuttle.conf -vvv

Example sshuttle call overriding the remote server:
sshuttle @/path/to/sshuttle.conf -r otheruser@test.example.com
2018-01-30 17:08:30 +11:00
9a176aa96f Update firewall.py 2018-01-01 09:35:41 +11:00
6b48301b86 move sdnotify after setting up firewall rules 2018-01-01 09:35:41 +11:00
be90cc8abd Fix tests on Macos
Swap hardcoded AF_INET(6) values for Python-provided values as they
differ between Darwin and Linux (30 vs 10 for AF_INET6 for instance).
2018-01-01 09:33:41 +11:00
512396e06b Add changelog entry about fixed license 2017-11-16 19:57:33 +11:00
7a71ae1380 Remove trailing whitespace 2017-11-16 18:06:33 +11:00
3a6f6cb795 Add changes entry for next release 2017-11-16 18:06:01 +11:00
81ab587698 Updating per @brianmay correspondence in https://github.com/sshuttle/sshuttle/issues/186 2017-11-16 18:05:39 +11:00
817284c2f8 Use more standard filename and format for bandit conifguration 2017-11-13 11:58:43 +11:00
71d65f3831 Fixes some style issues and minor bugs 2017-11-13 11:58:43 +11:00
9f238ebca8 Properly decode seed hosts argument in server.py
When I starting sshuttle with option `--seed-hosts example.com`, got the following error:

```
hostwatch: Starting hostwatch with Python version 3.5.2
hostwatch: Traceback (most recent call last):
--->   File "sshuttle.server", line 144, in start_hostwatch
--->   File "sshuttle.hostwatch", line 272, in hw_main
--->   File "sshuttle.hostwatch", line 234, in check_host
--->   File "sshuttle.hostwatch", line 32, in _is_ip
--->   File "/usr/lib/python3.5/re.py", line 163, in match
--->     return _compile(pattern, flags).match(string)
---> TypeError: cannot use a string pattern on a bytes-like object
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "assembler.py", line 37, in <module>
  File "sshuttle.server", line 393, in main
  File "sshuttle.ssnet", line 596, in runonce
  File "sshuttle.server", line 324, in hostwatch_ready
sshuttle.helpers.Fatal: hostwatch process died
```

It seems like the list of hosts is not properly decoded on the server side. This is an attempt to fix that.
2017-11-11 10:06:37 +11:00
9b315746d1 Using exec in the assembler is okay 2017-11-09 12:02:31 +11:00
6a488b3db9 Initial configuration for Bandit and Prospector
With this configuration it should be feasible to achieve a perfect score
without contortion.

Rules skiped for Bandit:
B101: assert_used
B104: hardcoded_bind_all_interfaces
B404: import_subprocess
B603: subprocess_without_shell_equals_true
B606: start_process_with_no_shell
B607: start_process_with_partial_path

Rules skiped for pylint:
- too-many-statements
- too-many-locals
- too-many-function-args
- too-many-arguments
- too-many-branches
- bare-except
- protected-access
- no-else-return
2017-11-09 12:02:31 +11:00
112931dd2c Changes methods that do not reference the instance to static methods 2017-11-08 16:17:06 +11:00
ad676029c7 Fix no value passed for argument auto_hosts in hw_main call 2017-11-08 16:17:06 +11:00
47030e846b Remove trailing whitespaces 2017-11-08 16:17:06 +11:00
416636fa9b Mock socket bind to avoid depending on local IPs being available in test box 2017-11-07 10:08:16 +11:00
4300a02343 Remove unused variable 'timeout' 2017-11-07 10:08:16 +11:00
4e8c5411b5 Also register por for dns proxy and for pairs in use by other procs 2017-11-07 10:08:16 +11:00
6cdc4da1e4 Fixes UDP and DNS proxies binding to the same socket address
As suggested by @colinmkeith the UDP and DNS proxies should listen on different
ports otherwise the DNS proxy can get traffic intended to the UDP proxy (or
vice-versa) and handle it incorrectly as reported in #178.

At first sight it seems that we had the code in place to try another port if
the one we are binding is already bound, however, with UDP and REUSEADDR the
OS will not refuse to bind two sockets to the same socket address, so both
the UDP proxy and DNS proxy were being bound to the same pair.
2017-11-07 10:08:16 +11:00
8add00866c turn off debugging 2017-10-23 06:58:21 +11:00
94ea0a3bed nested if should be and 2017-10-23 06:58:21 +11:00
9b7ce2811e Use versions of python3 greater than 3.5 when available (e.g. 3.6)
Some Linux distros, like Alpine, Arch, etc and some BSDs, like FreeBSD, are
now shipping with python3.6 as the default python3. Both the client and the
server are failing to run in this distros, because we are specifically looking
for python3.5.

These changes make the run shell script use python3 if the version is greater
than 3.5, otherwise falling back as usual.

On the server any version of python3 will do, use it before falling back to
python, as the server code can run with any version of python3.
2017-10-23 06:58:21 +11:00
7726dea27c Test double restore (ipv4, ipv6) disables only once; test kldload 2017-10-21 12:10:31 +11:00
3635cc17ad Load pf kernel module when enabling pf
When the pf module is not loaded our calls to pfctl will fail with
unhelpful messages.
This change spares the user the pain of decrypting those messages and manually
enabling pf. It also keeps track if pf was loaded by sshuttle and unloads on
exit if that was the case.

Also fixed the case where both ipv4 and ipv6 anchors were added by sshuttle
but the first call of disable would disable pf before the second call had the
chance of cleaning it's anchor.
2017-10-21 12:10:31 +11:00
ae13316e83 Just skip empty lines of routes data instead of stopping processing 2017-10-19 13:45:34 +11:00
e173eb6016 Skip empty lines on incoming routes data
If we receive no routes from server or if, for some reason, we receive
some empty lines, we should skip them instead of crashing.

Fixes on of the problems in #147.
2017-10-19 13:45:34 +11:00
29cd75b6f7 Make hostwatch find both fqdn and hostname
Currently hostwatch only adds hostnames even when FQDNs are available.
This commit changes found_host so that when the name is a FQDN, both the FQDN
and an hostname are added, e.g., given api.foo.com both api and api.foo.com
will be added.

Fixes #151 if merged.

N.B.: I rarely use hostwatch, it would probably be a good idea to get feedback
from people who actually use it before merging. Not too sure about this...
2017-10-17 07:12:06 +11:00
4c50be0bc7 Use getaddrinfo to obtain a correct sockaddr
While with AF_INET sockaddr is a 2-tuple composed by (address, port),
with AF_INET6 it is a 4-tuple with (address, port, flow info, scope id).

We were always passing a 2-tuple to socket.connect which would fail whenever
the address was, for instance, a link-local IPv6 address that needs a scope id.

With this change we now use getaddrinfo to correctly compute the full tuple.

Fixes #156.
2017-10-15 12:43:04 +11:00
max
2fa0cd06fb Route traffic by linux user 2017-09-17 15:33:34 +10:00
4d8b758d32 Add homebrew instructions
Per https://github.com/apenwarr/sshuttle/pull/45/files
2017-08-03 13:55:04 +10:00
4e8c2b9c68 Avoid port forwarding from loopback address
When doing port forwarding on lo0 avoid the special case where the
traffic on lo0 did not came from sshuttle pass out rule but from the lo0
address itself. Fixes #159.
2017-07-29 17:15:32 +10:00
be559fc78b Fix case where there is no --dns. 2017-07-18 17:15:03 +10:00
d2e97a60f7 Add new option for overriding destination DNS server. 2017-07-18 17:15:03 +10:00
cdbb379910 Talk to custom DNS server on pod, instead of the ones in /etc/resolv.conf 2017-07-18 17:15:03 +10:00
b65bb29023 Update changelog for 0.78.3 2017-07-09 09:12:04 +10:00
c093b4bd96 Get version for sphinx from sshuttle.version 2017-07-09 09:08:48 +10:00
e76d1e14bd Fix error in requirements.rst 2017-07-09 09:08:48 +10:00
6c6a39fefa Pin version in requirements.txt 2017-07-09 09:08:48 +10:00
714bd9f81b Update setup.cfg 2017-07-09 09:08:48 +10:00
c746d6f7db Update and reformat changelog 2017-07-09 09:08:48 +10:00
f9361d7014 Order first by port range and only then by swidth
This change makes the subnets with the most specific port ranges come
before subnets with larger, least specific, port ranges. Before this
change subnets with smaller swidth would always come first and only for
subnets with the same width would the size of the port range be
considered.

Example:
188.0.0.0/8 -x 0.0.0.0/0:443
Before: 188.0.0.0/8 would come first meaning that all ports would be
routed through the VPN for the subnet 188.0.0.0/8
After: 0.0.0.0/0:443 comes first, meaning that port 443 will be
excluded for all subnets, including 188.0.0.0/8. All other ports of
188.0.0.0/8 will be routed.
2017-05-08 16:56:42 +10:00
c4a41ada09 Adds support for tunneling specific port ranges (#144)
* Adds support for tunneling specific port ranges

This set of changes implements the ability of specifying a port or port
range for an IP or subnet to only tunnel those ports for that subnet.
Also supports excluding a port or port range for a given IP or subnet.

When, for a given subnet, there are intercepting ranges being added and
excluded, the most specific, i.e., smaller range, takes precedence. In
case of a tie the exclusion wins.

For different subnets, the most specific, i.e., largest swidth, takes
precedence independent of any eventual port ranges.

Examples:
Tunnels all traffic to the 188.0.0.0/8 subnet except those to port 443.
```
sshuttle -r <server> 188.0.0.0/8 -x 188.0.0.0/8:443
```

Only tunnels traffic to port 80 of the 188.0.0.0/8 subnet.
```
sshuttle -r <server> 188.0.0.0/8:80
```

Tunnels traffic to the 188.0.0.0/8 subnet and the port range that goes
from 80 to 89.
```
sshuttle -r <server> 188.0.0.0/8:80-89 -x 188.0.0.0/8:80-90
```

* Allow subnets to be specified with domain names

Simplifies the implementation of address parsing by using
socket.getaddrinfo(), which can handle domain resolution, IPv4 and IPv6
addresses. This was proposed and mostly implemented by @DavidBuchanan314
in #146.

Signed-off-by: David Buchanan <DavidBuchanan314@users.noreply.github.com>
Signed-off-by: João Vieira <vieira@yubo.be>

* Also use getaddrinfo for parsing listen addr:port

* Fixes tests for tunneling a port range

* Updates documentation to include port/port range

Adds some examples with subnet:port and subnet:port-port.
Also clarifies the versions of Python supported on the server while
maintaining the recommendation for Python 2.7, 3.5 or later.
Mentions support for pfSense.

* In Py2 only named arguments may follow *expression

Fixes issue in Python 2.7 where *expression may only be followed by
named arguments.

* Use right regex to extract ip4/6, mask and ports

* Tests for parse_subnetport
2017-05-07 13:18:13 +10:00
ef83a5c573 Work around non tabular headers in BSD netstat
netstat outputs some headers in BSD (that the Linux version does not)
that are not tabular and were breaking our 'split line into columns
and get nth column' logic. We now skip such headers.

Should fix #141.
2017-04-05 13:11:08 +10:00
af9ebd0f4b Fix UDP and DNS support on Python 2.7 with tproxy method
There was runtime failure on UDP or DNS processing, because "socket" was redefined to PyXAPI's socket_ext in tproxy.py, but still was plain Python's socket in client.py
Fixed https://github.com/sshuttle/sshuttle/issues/134 for me
2017-02-21 16:42:18 +11:00
9a9015a75e Fixed tests after adding support for iproute2 2017-02-11 09:07:50 +11:00
d7d24f956b Small refactoring of netstat/iproute parsing 2017-02-11 09:07:50 +11:00
809fad537f Add support for iproute2
`netstat` has been deprecated for some time and some distros might
start shipping without it in the near future. This commit adds support
for `ip route` and uses it when available.
2017-02-11 09:07:50 +11:00
abce18cfc2 Allow remote hosts with colons in the username 2017-02-11 09:02:28 +11:00
5e90491344 Re-introduce ipfw support for sshuttle on FreeBSD with support for --DNS option as well
Sponsored-by: rsync.net
2017-01-28 11:36:26 +11:00
e8ceccc3d5 Add support for PfSense
PfSense is based on FreeBSD and its pf is pretty close to the one
FreeBSD ships, however some structures have different fields and two
offsets had to be fixed.
2017-01-15 19:08:53 +11:00
e39c4afce0 Set started_by_sshuttle False after disabling pf
We set it to true when we enable pf, but do not set it back to False
after disabling. When using IPv4 and IPv6 we end up trying to disable
twice which procudes an error while undoing changes in FreeBSD 11.
2017-01-09 10:07:38 +11:00
0e52cce9d1 Fix punctuation and explain Type=notify
Added missing full stops and explain that Type=notify is needed in the
systemd service unit.
2016-10-30 10:58:03 +11:00
6d5d0d766f Tests and documentation for systemd integration
Some tests and documentation for the systemd notification feature.
Also fixes some corner case issues detected while writing the tests.
2016-10-30 10:58:03 +11:00
08fb3be7a0 Move pytest-runner to tests_require
As it is only required to run the tests move pytest-runner from
setup_requires to tests_require as suggested by @jonathanunderwood
on #115.
2016-10-29 12:04:22 +11:00
fee5868196 Fix warning: closed channel got=STOP_SENDING 2016-10-28 08:25:21 +11:00
fbbcc05d58 Support sdnotify for better systemd integration
These changes introduce support for sdnotify allowing sshuttle to notify
systemd when it finishes connecting to the server and installing
firewall rules, and is ready to tunnel requests.
2016-10-24 17:54:33 +11:00
15b394da86 Fix #117 to allow for no subnets via file (-s)
This should fix an issue introduced in #117 where when no subnets are
given via file (-s file) the variable is None instead of an empty list
and the concatenation with the subnets given as positional parameters
fails.
2016-10-13 17:52:58 +11:00
0ed5ef9a97 Fix argument splitting for multi-word arguments
By just splitting at spaces, multi-word arguments are torn apart even if
quoted. In case of custom ssh-cmd, this makes it practically impossible
to set certian options through `ssh -o`.
shlex splits arguments like a shell and e.g. respects quotes.
2016-10-04 18:19:59 +11:00
c0c3612e6d Allow subnets to be given only by file (-s)
This should fix #116. Handling this while still having the positional
arguments and -s both write to the same list turned out to be more
complicated than it's worth so each writes to their own variable and we
merge them at the end.
2016-09-27 08:12:39 +10:00
0033efca11 Merge pull request #113 from RichiH/patch-1
requirements.rst: Fix mistakes
2016-09-05 07:32:38 +10:00
ae6e25302f requirements.rst: Fix mistakes 2016-09-04 18:54:12 +02:00
ffd95fb776 Fix typo, space not required here 2016-09-01 18:38:13 +10:00
acb5aa5386 Update installation instructions
Closes #111.
2016-09-01 18:37:39 +10:00
4801ae6627 Support using run from different directory 2016-08-30 19:03:46 +10:00
f57ad356b9 Ensure we update sshuttle/version.py in run 2016-08-30 18:52:26 +10:00
a441a03e57 Don't print python version in run 2016-08-30 18:52:06 +10:00
d2fdb6c029 Add CWD to PYTHONPATH in run 2016-08-30 18:51:19 +10:00
2c20a1fd5a New release 2016-08-06 18:58:00 +10:00
915f72de35 Add changes for next release 2016-08-06 18:52:26 +10:00
1ffc3f52a1 Merge pull request #108 from vieira/pf-ipv6
IPv6 support for OSX and BSDs
2016-07-29 07:57:35 +10:00
8520ea2787 Use == instead of is to compare with AF_INET 2016-07-27 23:18:25 +00:00
6a394deaf2 Fixes missing comma from tuple in pf tests 2016-07-27 23:06:36 +00:00
83d5c59a57 Tests for IPv6 on pf 2016-07-27 22:17:02 +00:00
1cfd9eb9d7 Be more specific and consistent in some pf rules 2016-07-27 22:15:47 +00:00
f8d58fa4f0 IPv6 support for BSD and OSX
Adds IPv6 support for OpenBSD and OSX.
2016-07-24 22:04:29 +00:00
d2d5a37541 AF_INET6 is different between BSDs and Linux
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.
2016-07-24 22:02:17 +00:00
e9be2deea0 Exclude the IP where sshuttle is really listening
We were always excluding 127.0.0.1/8 but sshuttle might be listening on
other IP, e.g., ::1 for IPv6 or any other defined with -l
2016-07-24 21:58:20 +00:00
22b1b54bfd Add pytest-runner support 2016-07-10 11:26:32 +10:00
a43c668dde Fixes type mismatch between str and bytes
Should fix issue #104.
2016-07-09 22:49:12 +00:00
e0dfb95596 Fix OpenBSD pf test failure 2016-06-17 17:18:43 +08:00
5d28ce8272 Merge pull request #1 from vieira/patch-1
Add <forward_subnets> to divert rule in OpenBSD
2016-06-17 08:25:59 +08:00
f876c5db5e Add <forward_subnets> to divert rule in OpenBSD
Fixes bug where all traffic routed to loopback would end up being diverted to the same port.
2016-06-16 22:34:19 +01:00
2e1beefc9a Hack pf to enable multiple instances in Mac OS X 10.10 and above 2016-06-16 12:31:02 +08:00
5a20783baa tweak docs to match @vieira's changes 2016-05-02 21:40:53 -07:00
495b3c39ea Seed hosts without auto hosts
A possible implementation for the change requested in #94, so that seed
hosts can be used without auto hosts. In this scenario only the
specified hosts (or ips) will be looked up (or rev looked up).
2016-05-03 00:18:32 +00:00
f3cbc5018a Fix PEP8 issues 2016-04-30 18:08:46 +10:00
e73e797f33 Update files list 2016-04-30 18:05:47 +10:00
1d64879613 Fix tests 2016-04-23 13:19:06 +10:00
8fad282bfd Ensure locale is set to C for external commands
Otherwise the output can vary and confuse our attempts to parse it.

Fixes: 93
2016-04-23 12:53:45 +10:00
1dda9dd621 Add ENETUNREACH to NET_ERRS
We shouldn't come up with a fatal error because of a ENETUNREACH when
trying to contact the DNS server. Although this error shouldn't happen
either.

Fixes #89.
2016-04-20 15:18:59 +10:00
74e308a29f Don't mix tab and spaces in shell script
Sometime ago I was in python mode and incorrectly indented a line of the
shell script with spaces instead of tabs. Shame on me. This should bring
things back to their natural order.
2016-04-20 15:17:07 +10:00
516ff7bc4a Correctly obtains the python executable to use
Previously the sshuttle shell script would pass the python to use as the
first argument of the command. The new run script no longer does this.
Instead we can obtain the python being used via sys.executable.
Fixes #88.
2016-04-20 15:15:44 +10:00
89c5b57019 Attempt readthedocs workaround
readthedocs alters docs/conf.py which in turn means python_scm detects a
version and incorrectly adjusts the version number. Here we try to work
around this problem.

We do this by renaming the docs/conf.py file and copying it back to
docs/conf.py when setup.py is invoked. This way, hopefully, scm won't
see the changes to docs/conf.py

References:
http://stackoverflow.com/questions/35811267/readthedocs-and-setuptools-scm-version-wrong/36386177
https://github.com/pypa/setuptools_scm/issues/84
2016-04-18 11:44:05 +10:00
45 changed files with 2034 additions and 716 deletions

12
.gitignore vendored
View File

@ -1,7 +1,17 @@
sshuttle/version.py /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
View 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

View File

@ -1,13 +1,19 @@
language: python language: python
python: python:
- 2.6
- 2.7 - 2.7
- 3.4 - 3.4
- 3.5 - 3.5
- 3.6
- pypy - pypy
install: install:
- travis_retry pip install -q pytest mock - travis_retry pip install -q -r requirements-tests.txt
before_script:
# stop the build if there are Python syntax errors or undefined names.
- flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide.
- flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
script: script:
- PYTHONPATH=. py.test - PYTHONPATH=. py.test

View File

@ -1,5 +1,119 @@
Release 0.78.0 (Apr 8, 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/
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. * Don't force IPv6 if IPv6 nameservers supplied. Fixes #74.
* Call /bin/sh as users shell may not be POSIX compliant. Fixes #77. * Call /bin/sh as users shell may not be POSIX compliant. Fixes #77.
@ -9,22 +123,22 @@ Release 0.78.0 (Apr 8, 2016)
* Make server parts work with old versions of Python. Fixes #81. * Make server parts work with old versions of Python. Fixes #81.
Release 0.77.2 (Mar 7, 2016) 0.77.2 - 2016-03-07
============================ -------------------
* Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed. * Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed.
Release 0.77.1 (Mar 7, 2016) 0.77.1 - 2016-03-07
============================ -------------------
* Use semantic versioning. http://semver.org/ * Use semantic versioning. http://semver.org/
* Update GPL 2 license text. * Update GPL 2 license text.
* New release to fix PyPI. * New release to fix PyPI.
Release 0.77 (Mar 3, 2016) 0.77 - 2016-03-03
========================== -----------------
* Various bug fixes. * Various bug fixes.
* Fix Documentation. * Fix Documentation.
@ -32,8 +146,8 @@ Release 0.77 (Mar 3, 2016)
* Add support for OpenBSD. * Add support for OpenBSD.
Release 0.76 (Jan 17, 2016) 0.76 - 2016-01-17
=========================== -----------------
* Add option to disable IPv6 support. * Add option to disable IPv6 support.
* Update documentation. * Update documentation.
@ -41,14 +155,14 @@ Release 0.76 (Jan 17, 2016)
* Use setuptools-scm for automatic versioning. * Use setuptools-scm for automatic versioning.
Release 0.75 (Jan 12, 2016) 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.

View File

@ -11,3 +11,4 @@ recursive-include docs *.py
recursive-include docs *.rst recursive-include docs *.rst
recursive-include docs Makefile recursive-include docs Makefile
recursive-include sshuttle *.py recursive-include sshuttle *.py
recursive-exclude docs/_build *

View File

@ -29,15 +29,41 @@ common case:
Obtaining sshuttle Obtaining sshuttle
------------------ ------------------
- Debian stretch or later::
apt-get install 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
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::
brew install sshuttle
Documentation Documentation
------------- -------------
The documentation for the stable version is available at: The documentation for the stable version is available at:

9
bandit.yml Normal file
View File

@ -0,0 +1,9 @@
exclude_dirs:
- sshuttle/tests
skips:
- B101
- B104
- B404
- B603
- B606
- B607

View File

@ -1,4 +1 @@
Changelog
---------
.. include:: ../CHANGES.rst .. include:: ../CHANGES.rst

View File

@ -13,8 +13,10 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
# import sys import sys
# import os 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, # 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 # add these directories to sys.path here. If the directory is relative to the
@ -53,11 +55,10 @@ copyright = '2016, Brian May'
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version.
from setuptools_scm import get_version
version = get_version(root="..")
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = sshuttle.version.version
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

View File

@ -8,4 +8,5 @@ Installation
- Clone:: - Clone::
git clone https://github.com/sshuttle/sshuttle.git git clone https://github.com/sshuttle/sshuttle.git
cd sshuttle
./setup.py install ./setup.py install

View File

@ -31,11 +31,18 @@ Options
.. option:: subnets .. option:: subnets
A list of subnets to route over the VPN, in the form A list of subnets to route over the VPN, in the form
``a.b.c.d[/width]``. Valid examples are 1.2.3.4 (a ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4), single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
netmask), and 0/0 ('just route everything through the netmask), and 0/0 ('just route everything through the
VPN'). 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|tproxy|pf] .. option:: --method [auto|nat|tproxy|pf]
@ -54,9 +61,11 @@ Options
connections from other machines on your network (ie. to connections from other machines on your network (ie. to
run :program:`sshuttle` on a router) try enabling IP Forwarding in run :program:`sshuttle` on a router) try enabling IP Forwarding in
your kernel, then using ``--listen 0.0.0.0:0``. 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 method this can be an IPv6 address. Use this option twice if For the tproxy and pf methods this can be an IPv6 address. Use this option
required, to provide both IPv4 and IPv6 addresses. twice if required, to provide both IPv4 and IPv6 addresses.
.. option:: -H, --auto-hosts .. option:: -H, --auto-hosts
@ -136,6 +145,10 @@ Options
if you use this option to give it a few names to start if you use this option to give it a few names to start
from. from.
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 .. option:: --no-latency-control
Sacrifice latency to improve bandwidth benchmarks. ssh Sacrifice latency to improve bandwidth benchmarks. ssh
@ -172,7 +185,7 @@ Options
.. option:: --disable-ipv6 .. option:: --disable-ipv6
If using the tproxy method, this will disable IPv6 support. If using tproxy or pf methods, this will disable IPv6 support.
.. option:: --firewall .. option:: --firewall
@ -192,6 +205,29 @@ Options
feature. feature.
Configuration File
------------------
All the options described above can optionally be specified in a configuration
file.
To run :program:`sshuttle` with options defined in, e.g., `/etc/ssshuttle.conf`
just pass the path to the file preceded by the `@` character, e.g.
:option:`@/etc/ssshuttle.conf`.
When running :program:`sshuttle` with options defined in a configuratio 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 Examples
-------- --------
Test locally by proxying all local connections, without using ssh:: Test locally by proxying all local connections, without using ssh::
@ -240,6 +276,24 @@ and subnet guessing::
c : Keyboard interrupt: exiting. c : Keyboard interrupt: exiting.
c : SW#6:192.168.42.121:60554: deleting 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 Discussion
---------- ----------

View File

@ -4,7 +4,7 @@ Overview
As far as I know, sshuttle is the only program that solves the following As far as I know, sshuttle is the only program that solves the following
common case: common case:
- Your client machine (or router) is Linux, FreeBSD, or MacOS. - Your client machine (or router) is Linux, MacOS, FreeBSD, OpenBSD or pfSense.
- You have access to a remote network via ssh. - You have access to a remote network via ssh.

View File

@ -26,28 +26,31 @@ Linux with TPROXY method
Supports: Supports:
* IPv4 TCP * IPv4 TCP
* IPv4 UDP (requires ``recmsg`` - see below) * IPv4 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recmsg`` - see below) * IPv6 DNS (requires ``recvmsg`` - see below)
* IPv6 TCP * IPv6 TCP
* IPv6 UDP (requires ``recmsg`` - see below) * IPv6 UDP (requires ``recvmsg`` - see below)
* IPv6 DNS (requires ``recmsg`` - see below) * IPv6 DNS (requires ``recvmsg`` - see below)
.. _PyXAPI: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/ .. _PyXAPI: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
Full UDP or DNS support with the TPROXY method requires the ``recvmsg()`` Full UDP or DNS support with the TPROXY method requires the ``recvmsg()``
syscall. This is not available in Python 2, however is in Python 3.5 and syscall. This is not available in Python 2, however it is in Python 3.5 and
later. Under Python 2 you might find it sufficient installing PyXAPI_ to get later. Under Python 2 you might find it sufficient to install PyXAPI_ in
the ``recvmsg()`` function. See :doc:`tproxy` for more information. order to get the ``recvmsg()`` function. See :doc:`tproxy` for more
information.
MacOS / FreeBSD / OpenBSD MacOS / FreeBSD / OpenBSD / pfSense
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Method: pf Method: pf
Supports: Supports:
* IPv4 TCP * IPv4 TCP
* IPv4 DNS * IPv4 DNS
* IPv6 TCP
* IPv6 DNS
Requires: Requires:
@ -62,12 +65,31 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
Server side Requirements Server side Requirements
------------------------ ------------------------
Server requirements are more relaxed, however it is recommended that you use The server can run in any version of Python between 2.4 and 3.6.
Python 2.7 or Python 3.5. However it is recommended that you use Python 2.7, Python 3.5 or later whenever
possible as support for older versions might be dropped in the future.
Additional Suggested Software Additional Suggested Software
----------------------------- -----------------------------
- You may want to use autossh, available in various package management - You may want to use autossh, available in various package management
systems systems.
- If you are using systemd, sshuttle can notify it when the connection to
the remote end is established and the firewall rules are installed. For
this feature to work you must configure the process start-up type for the
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

4
requirements-tests.txt Normal file
View File

@ -0,0 +1,4 @@
-r requirements.txt
pytest==3.4.2
mock==2.0.0
flake8==3.5.0

View File

@ -1 +1 @@
setuptools_scm setuptools-scm==1.15.6

20
run
View File

@ -1,8 +1,16 @@
#!/bin/sh #!/usr/bin/env sh
if python3.5 -V 2>/dev/null; then set -e
exec python3.5 -m "sshuttle" "$@" export PYTHONPATH="$(dirname $0):$PYTHONPATH"
elif python2.7 -V 2>/dev/null; then
exec python2.7 -m "sshuttle" "$@" python_best_version() {
if [ -x "$(command -v python3)" ] &&
python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then
exec python3 "$@"
elif [ -x "$(command -v python2.7)" ]; then
exec python2.7 "$@"
else else
exec python -m "sshuttle" "$@" exec python "$@"
fi fi
}
python_best_version -m "sshuttle" "$@"

9
setup.cfg Normal file
View File

@ -0,0 +1,9 @@
[aliases]
test=pytest
[bdist_wheel]
universal = 1
[upload]
sign=true
identity=0x1784577F811F6EAC

View File

@ -2,20 +2,20 @@
# 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
@ -25,6 +25,7 @@ def version_scheme(version):
version = guess_next_dev_version(version) version = guess_next_dev_version(version)
return version.lstrip("v") return version.lstrip("v")
setup( setup(
name="sshuttle", name="sshuttle",
use_scm_version={ use_scm_version={
@ -38,14 +39,14 @@ setup(
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(),
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 :: 2.7",
"Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.5",
@ -56,6 +57,6 @@ setup(
'sshuttle = sshuttle.cmdline:main', 'sshuttle = sshuttle.cmdline:main',
], ],
}, },
tests_require=['pytest', 'mock'], tests_require=['pytest', 'pytest-runner', 'mock'],
keywords="ssh vpn", keywords="ssh vpn",
) )

View File

@ -2,6 +2,7 @@ import sys
import zlib import zlib
import imp import imp
verbosity = verbosity # noqa: F821 must be a previously defined global
z = zlib.decompressobj() z = zlib.decompressobj()
while 1: while 1:
name = sys.stdin.readline().strip() name = sys.stdin.readline().strip()
@ -21,7 +22,7 @@ while 1:
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
@ -34,4 +35,4 @@ sshuttle.helpers.verbose = verbosity
import sshuttle.cmdline_options as options import sshuttle.cmdline_options as options
from sshuttle.server import main from sshuttle.server import main
main(options.latency_control) main(options.latency_control, options.auto_hosts, options.to_nameserver)

View File

@ -1,4 +1,3 @@
import socket
import errno import errno
import re import re
import signal import signal
@ -15,6 +14,24 @@ 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
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('/dev/null', os.O_RDONLY) _extra_fd = os.open('/dev/null', os.O_RDONLY)
@ -94,8 +111,8 @@ 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.type = type self.type = kind
self.proto = proto self.proto = proto
self.v6 = None self.v6 = None
self.v4 = None self.v4 = None
@ -143,13 +160,11 @@ class MultiListener:
self.bind_called = True self.bind_called = True
if address_v6 is not None: if address_v6 is not None:
self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto) self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto)
self.v6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.v6.bind(address_v6) self.v6.bind(address_v6)
else: else:
self.v6 = None self.v6 = None
if address_v4 is not None: if address_v4 is not None:
self.v4 = socket.socket(socket.AF_INET, self.type, self.proto) self.v4 = socket.socket(socket.AF_INET, self.type, self.proto)
self.v4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.v4.bind(address_v4) self.v4.bind(address_v4)
else: else:
self.v4 = None self.v4 = None
@ -178,8 +193,8 @@ class FirewallClient:
if ssyslog._p: if ssyslog._p:
argvbase += ['--syslog'] argvbase += ['--syslog']
argv_tries = [ argv_tries = [
['sudo', '-p', '[local sudo] Password: ', ['sudo', '-p', '[local sudo] Password: ', '/usr/bin/env',
('PYTHONPATH=%s' % python_path), '--'] + argvbase, ('PYTHONPATH=%s' % python_path)] + argvbase,
argvbase argvbase
] ]
@ -224,7 +239,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
@ -233,6 +249,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()
@ -241,12 +258,13 @@ 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'
for (family, ip, width) in self.subnets_exclude: % (family, width, ip.encode("ASCII"), fport, lport))
self.pfile.write(b'%d,%d,1,%s\n' for (family, ip, width, fport, lport) in self.subnets_exclude:
% (family, width, ip.encode("ASCII"))) 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:
@ -261,8 +279,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()
@ -271,7 +295,7 @@ 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(b'[^-\w\.]', hostname))
assert(not re.search(b'[^0-9.]', ip)) assert(not re.search(b'[^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()
@ -362,7 +386,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(
@ -400,7 +424,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())
@ -418,7 +443,9 @@ 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))
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)")
@ -459,6 +486,7 @@ 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)
@ -469,7 +497,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!
@ -514,8 +542,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):
if daemon: if daemon:
try: try:
@ -530,6 +559,11 @@ def main(listenip_v6, listenip_v4,
# 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]
@ -547,9 +581,19 @@ def main(listenip_v6, listenip_v4,
else: else:
listenip_v6 = None listenip_v6 = None
if user 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)
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
required.udp = avail.udp required.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 IPv6 not supported, ignore IPv6 DNS servers
if not required.ipv6: if not required.ipv6:
@ -565,17 +609,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
@ -617,10 +673,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
@ -640,6 +698,8 @@ 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:
@ -659,10 +719,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')
@ -678,22 +740,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")
@ -707,13 +769,13 @@ 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:

View File

@ -1,10 +1,11 @@
import re import re
import socket
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
import sshuttle.client as client import sshuttle.client as client
import sshuttle.firewall as firewall import sshuttle.firewall as firewall
import sshuttle.hostwatch as hostwatch import sshuttle.hostwatch as hostwatch
import sshuttle.ssyslog as ssyslog import sshuttle.ssyslog as ssyslog
from sshuttle.options import parser, parse_ipport6, parse_ipport4 from sshuttle.options import parser, parse_ipport
from sshuttle.helpers import family_ip_tuple, log, Fatal from sshuttle.helpers import family_ip_tuple, log, Fatal
@ -20,22 +21,21 @@ def main():
try: try:
if opt.firewall: if opt.firewall:
if opt.subnets: if opt.subnets or opt.subnets_file:
parser.error('exactly zero arguments expected') parser.error('exactly zero arguments expected')
return firewall.main(opt.method, opt.syslog) return firewall.main(opt.method, opt.syslog)
elif opt.hostwatch: elif opt.hostwatch:
return hostwatch.hw_main(opt.subnets) return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
else: else:
includes = opt.subnets includes = opt.subnets + opt.subnets_file
excludes = opt.exclude excludes = opt.exclude
if not includes and not opt.auto_nets: if not includes and not opt.auto_nets:
parser.error('at least one subnet, subnet file, or -N expected') parser.error('at least one subnet, subnet file, '
'or -N expected')
remotename = opt.remote remotename = opt.remote
if remotename == '' or remotename == '-': if remotename == '' or remotename == '-':
remotename = None remotename = None
nslist = [family_ip_tuple(ns) for ns in opt.ns_hosts] nslist = [family_ip_tuple(ns) for ns in opt.ns_hosts]
if opt.seed_hosts and not opt.auto_hosts:
parser.error('--seed-hosts only works if you also use -H')
if opt.seed_hosts: if opt.seed_hosts:
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip()) sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
elif opt.auto_hosts: elif opt.auto_hosts:
@ -45,12 +45,13 @@ def main():
if opt.listen: if opt.listen:
ipport_v6 = None ipport_v6 = None
ipport_v4 = None ipport_v4 = None
list = opt.listen.split(",") lst = opt.listen.split(",")
for ip in list: for ip in lst:
if '[' in ip and ']' in ip: family, ip, port = parse_ipport(ip)
ipport_v6 = parse_ipport6(ip) if family == socket.AF_INET6:
ipport_v6 = (ip, port)
else: else:
ipport_v4 = parse_ipport4(ip) ipport_v4 = (ip, port)
else: else:
# parse_ipport4('127.0.0.1:0') # parse_ipport4('127.0.0.1:0')
ipport_v4 = "auto" ipport_v4 = "auto"
@ -68,10 +69,14 @@ def main():
nslist, nslist,
opt.method, opt.method,
sh, sh,
opt.auto_hosts,
opt.auto_nets, opt.auto_nets,
includes, includes,
excludes, excludes,
opt.daemon, opt.pidfile) opt.daemon,
opt.to_ns,
opt.pidfile,
opt.user)
if return_code == 0: if return_code == 0:
log('Normal exit code, exiting...') log('Normal exit code, exiting...')

View File

@ -2,6 +2,7 @@ import errno
import socket import socket
import signal import signal
import sshuttle.ssyslog as ssyslog import sshuttle.ssyslog as ssyslog
import sshuttle.sdnotify as sdnotify
import sys import sys
import os import os
import platform import platform
@ -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) = \
line.strip().split(',', 5)
except: except:
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 = []
@ -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,19 +204,23 @@ 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')
sdnotify.send(sdnotify.ready(),
sdnotify.status('Connected'))
try: try:
stdout.flush() stdout.flush()
@ -221,14 +246,15 @@ def main(method_name, syslog):
break break
finally: finally:
try: try:
sdnotify.send(sdnotify.stop())
debug1('firewall manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')
except: except:
pass pass
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:
try: try:
debug1("firewall manager: " debug1("firewall manager: "
@ -239,9 +265,9 @@ def main(method_name, syslog):
pass pass
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:
try: try:
debug1("firewall manager: " debug1("firewall manager: "

View File

@ -16,6 +16,7 @@ else:
def b(s): def b(s):
return s return s
def log(s): def log(s):
global logprefix global logprefix
try: try:

View File

@ -61,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()
@ -247,7 +251,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:
@ -255,7 +259,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:
@ -264,16 +268,17 @@ 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() for h in seed_hosts:
check_host(h)
if auto_hosts:
read_host_cache()
_enqueue(_check_etc_hosts) _enqueue(_check_etc_hosts)
_enqueue(_check_netstat) _enqueue(_check_netstat)
check_host('localhost') check_host('localhost')
check_host(socket.gethostname()) check_host(socket.gethostname())
check_workgroup('workgroup') check_workgroup('workgroup')
check_workgroup('-') check_workgroup('-')
for h in seed_hosts:
check_host(h)
while 1: while 1:
now = time.time() now = time.time()

View File

@ -1,3 +1,5 @@
import re
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,7 +20,11 @@ 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 = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
for line in p.stdout: for line in p.stdout:
if line.startswith(b'Chain %s ' % name.encode("ASCII")): if line.startswith(b'Chain %s ' % name.encode("ASCII")):
return True return True
@ -35,7 +41,44 @@ 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 == socket.AF_INET:
argv = ['nft', action, 'ip', table] + list(args)
elif family == socket.AF_INET6:
argv = ['nft', action, 'ip6', 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:
raise Fatal('%r returned %d' % (argv, rv))
def nft_get_handle(expression, chain):
cmd = 'nft'
argv = [cmd, 'list', expression, '-a']
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
for line in p.stdout:
if (b'jump %s' % chain.encode('utf-8')) in line:
return re.sub('.*# ', '', line.decode('utf-8'))
rv = p.wait()
if rv: if rv:
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))

View File

@ -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)

257
sshuttle/methods/ipfw.py Normal file
View File

@ -0,0 +1,257 @@
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('/dev/null', 'w'))
_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)
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)
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')

View File

@ -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,50 @@ 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)
# 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 +90,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

80
sshuttle/methods/nft.py Normal file
View File

@ -0,0 +1,80 @@
import socket
from sshuttle.firewall import subnet_weight
from sshuttle.linux import nft, nft_get_handle, 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 = "nat"
def _nft(action, *args):
return nft(family, table, action, *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
_nft('add table', '')
_nft('add chain', 'prerouting',
'{ type nat hook prerouting priority -100; policy accept; }')
_nft('add chain', 'postrouting',
'{ type nat hook postrouting priority 100; policy accept; }')
_nft('add chain', 'output',
'{ type nat hook output priority -100; policy accept; }')
_nft('add chain', chain)
_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:
tcp_ports = tcp_ports + ('dport { %d-%d }' % (fport, lport))
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 = "nat"
def _nft(action, *args):
return nft(family, table, action, *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
handle = nft_get_handle('chain ip nat output', chain)
nonfatal(_nft, 'delete rule', 'output', handle)
handle = nft_get_handle('chain ip nat prerouting', chain)
nonfatal(_nft, 'delete rule', 'prerouting', handle)
nonfatal(_nft, 'delete chain', chain)

View File

@ -1,17 +1,24 @@
import os import os
import sys import sys
import platform
import re import re
import socket import socket
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
_pf_context = {'started_by_sshuttle': False, 'Xtoken': None} _pf_context = {
'started_by_sshuttle': 0,
'loaded_by_sshuttle': True,
'Xtoken': []
}
_pf_fd = None _pf_fd = None
@ -57,11 +64,14 @@ class Generic(object):
def enable(self): def enable(self):
if b'INFO:\nStatus: Disabled' in self.status: if b'INFO:\nStatus: Disabled' in self.status:
pfctl('-e') pfctl('-e')
_pf_context['started_by_sshuttle'] = True _pf_context['started_by_sshuttle'] += 1
def disable(self): @staticmethod
if _pf_context['started_by_sshuttle']: def disable(anchor):
pfctl('-a %s -F all' % anchor)
if _pf_context['started_by_sshuttle'] == 1:
pfctl('-d') pfctl('-d')
_pf_context['started_by_sshuttle'] -= 1
def query_nat(self, family, proto, src_ip, src_port, dst_ip, dst_port): def query_nat(self, family, proto, src_ip, src_port, dst_ip, dst_port):
[proto, family, src_port, dst_port] = [ [proto, family, src_port, dst_port] = [
@ -89,28 +99,30 @@ class Generic(object):
port = socket.ntohs(self._get_natlook_port(pnl.rdxport)) port = socket.ntohs(self._get_natlook_port(pnl.rdxport))
return (ip, port) return (ip, port)
def _add_natlook_ports(self, pnl, src_port, dst_port): @staticmethod
def _add_natlook_ports(pnl, src_port, dst_port):
pnl.sxport = socket.htons(src_port) pnl.sxport = socket.htons(src_port)
pnl.dxport = socket.htons(dst_port) pnl.dxport = socket.htons(dst_port)
def _get_natlook_port(self, xport): @staticmethod
def _get_natlook_port(xport):
return xport return xport
def add_anchors(self, status=None): def add_anchors(self, anchor, status=None):
if status is None: if status is None:
status = pfctl('-s all')[0] status = pfctl('-s all')[0]
self.status = status self.status = status
if b'\nanchor "sshuttle"' not in status: if ('\nanchor "%s"' % anchor).encode('ASCII') not in status:
self._add_anchor_rule(self.PF_PASS, b"sshuttle") self._add_anchor_rule(self.PF_PASS, anchor.encode('ASCII'))
def _add_anchor_rule(self, type, name, pr=None): def _add_anchor_rule(self, kind, name, pr=None):
if pr is None: if pr is None:
pr = self.pfioc_rule() pr = self.pfioc_rule()
memmove(addressof(pr) + self.ANCHOR_CALL_OFFSET, name, memmove(addressof(pr) + self.ANCHOR_CALL_OFFSET, name,
min(self.MAXPATHLEN, len(name))) # anchor_call = name min(self.MAXPATHLEN, len(name))) # anchor_call = name
memmove(addressof(pr) + self.RULE_ACTION_OFFSET, memmove(addressof(pr) + self.RULE_ACTION_OFFSET,
struct.pack('I', type), 4) # rule.action = type struct.pack('I', kind), 4) # rule.action = kind
memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack( memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack(
'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET 'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
@ -120,10 +132,24 @@ class Generic(object):
'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL 'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)
def add_rules(self, rules): @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) assert isinstance(rules, bytes)
debug3("rules:\n" + rules.decode("ASCII")) debug3("rules:\n" + rules.decode("ASCII"))
pfctl('-a sshuttle -f /dev/stdin', rules) 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): class FreeBsd(Generic):
@ -150,52 +176,66 @@ class FreeBsd(Generic):
freebsd.pfioc_natlook = pfioc_natlook freebsd.pfioc_natlook = pfioc_natlook
return freebsd return freebsd
def __init__(self): def enable(self):
super(FreeBsd, self).__init__() returncode = ssubprocess.call(['kldload', 'pf'])
super(FreeBsd, self).enable()
if returncode == 0:
_pf_context['loaded_by_sshuttle'] = True
def add_anchors(self): 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'])
def add_anchors(self, anchor):
status = pfctl('-s all')[0] status = pfctl('-s all')[0]
if b'\nrdr-anchor "sshuttle"' not in status: if ('\nrdr-anchor "%s"' % anchor).encode('ASCII') not in status:
self._add_anchor_rule(self.PF_RDR, b'sshuttle') self._add_anchor_rule(self.PF_RDR, anchor.encode('ASCII'))
super(FreeBsd, self).add_anchors(status=status) super(FreeBsd, self).add_anchors(anchor, status=status)
def _add_anchor_rule(self, type, name): def _add_anchor_rule(self, kind, name, pr=None):
pr = self.pfioc_rule() pr = pr or self.pfioc_rule()
ppa = self.pfioc_pooladdr() ppa = self.pfioc_pooladdr()
ioctl(pf_get_dev(), self.DIOCBEGINADDRS, ppa) ioctl(pf_get_dev(), self.DIOCBEGINADDRS, ppa)
# pool ticket # pool ticket
memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4) memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4)
super(FreeBsd, self)._add_anchor_rule(type, name, pr=pr) super(FreeBsd, self)._add_anchor_rule(kind, name, pr=pr)
def add_rules(self, includes, port, dnsport, nslist): def add_rules(self, anchor, includes, port, dnsport, nslist, family):
tables = [ inet_version = self._inet_version(family)
b'table <forward_subnets> {%s}' % b','.join(includes) lo_addr = self._lo_addr(family)
]
tables = []
translating_rules = [ translating_rules = [
b'rdr pass on lo0 proto tcp ' b'rdr pass on lo0 %s proto tcp from ! %s to %s '
b'to <forward_subnets> -> 127.0.0.1 port %r' % port b'-> %s port %r' % (inet_version, lo_addr, subnet, lo_addr, port)
for exclude, subnet in includes if not exclude
] ]
filtering_rules = [ filtering_rules = [
b'pass out route-to lo0 inet proto tcp ' b'pass out route-to lo0 %s proto tcp '
b'to <forward_subnets> keep state' b'to %s keep state' % (inet_version, subnet)
if not exclude else
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
for exclude, subnet in includes
] ]
if len(nslist) > 0: if nslist:
tables.append( tables.append(
b'table <dns_servers> {%s}' % b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist])) b','.join([ns[1].encode("ASCII") for ns in nslist]))
translating_rules.append( translating_rules.append(
b'rdr pass on lo0 proto udp to ' b'rdr pass on lo0 %s proto udp to <dns_servers> '
b'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport) b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport))
filtering_rules.append( filtering_rules.append(
b'pass out route-to lo0 inet proto udp to ' b'pass out route-to lo0 %s proto udp to '
b'<dns_servers> port 53 keep state') b'<dns_servers> port 53 keep state' % inet_version)
rules = b'\n'.join(tables + translating_rules + filtering_rules) \ rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ b'\n' + b'\n'
super(FreeBsd, self).add_rules(rules) super(FreeBsd, self).add_rules(anchor, rules)
class OpenBsd(Generic): class OpenBsd(Generic):
@ -225,41 +265,47 @@ class OpenBsd(Generic):
self.pfioc_natlook = pfioc_natlook self.pfioc_natlook = pfioc_natlook
super(OpenBsd, self).__init__() super(OpenBsd, self).__init__()
def add_anchors(self): def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo # before adding anchors and rules we must override the skip lo
# that comes by default in openbsd pf.conf so the rules we will add, # that comes by default in openbsd pf.conf so the rules we will add,
# which rely on translating/filtering packets on lo, can work # which rely on translating/filtering packets on lo, can work
if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'match on lo\n') pfctl('-f /dev/stdin', b'match on lo\n')
super(OpenBsd, self).add_anchors() super(OpenBsd, self).add_anchors(anchor)
def add_rules(self, includes, port, dnsport, nslist): def add_rules(self, anchor, includes, port, dnsport, nslist, family):
tables = [ inet_version = self._inet_version(family)
b'table <forward_subnets> {%s}' % b','.join(includes) lo_addr = self._lo_addr(family)
]
tables = []
translating_rules = [ translating_rules = [
b'pass in on lo0 inet proto tcp ' b'pass in on lo0 %s proto tcp to %s '
b'divert-to 127.0.0.1 port %r' % port b'divert-to %s port %r' % (inet_version, subnet, lo_addr, port)
for exclude, subnet in includes if not exclude
] ]
filtering_rules = [ filtering_rules = [
b'pass out inet proto tcp ' b'pass out %s proto tcp to %s '
b'to <forward_subnets> route-to lo0 keep state' b'route-to lo0 keep state' % (inet_version, subnet)
if not exclude else
b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
for exclude, subnet in includes
] ]
if len(nslist) > 0: if nslist:
tables.append( tables.append(
b'table <dns_servers> {%s}' % b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist])) b','.join([ns[1].encode("ASCII") for ns in nslist]))
translating_rules.append( translating_rules.append(
b'pass in on lo0 inet proto udp to <dns_servers>' b'pass in on lo0 %s proto udp to <dns_servers> port 53 '
b'port 53 rdr-to 127.0.0.1 port %r' % dnsport) b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport))
filtering_rules.append( filtering_rules.append(
b'pass out inet proto udp to ' b'pass out %s proto udp to <dns_servers> port 53 '
b'<dns_servers> port 53 route-to lo0 keep state') b'route-to lo0 keep state' % inet_version)
rules = b'\n'.join(tables + translating_rules + filtering_rules) \ rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ b'\n' + b'\n'
super(OpenBsd, self).add_rules(rules) super(OpenBsd, self).add_rules(anchor, rules)
class Darwin(FreeBsd): class Darwin(FreeBsd):
@ -292,19 +338,20 @@ class Darwin(FreeBsd):
def enable(self): def enable(self):
o = pfctl('-E') o = pfctl('-E')
_pf_context['Xtoken'] = \ _pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1))
re.search(b'Token : (.+)', o[1]).group(1)
def disable(self): def disable(self, anchor):
if _pf_context['Xtoken'] is not None: pfctl('-a %s -F all' % anchor)
pfctl('-X %s' % _pf_context['Xtoken'].decode("ASCII")) if _pf_context['Xtoken']:
pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII"))
def add_anchors(self): def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo # before adding anchors and rules we must override the skip lo
# that in some cases ends up in the chain so the rules we will add, # that in some cases ends up in the chain so the rules we will add,
# which rely on translating/filtering packets on lo, can work # which rely on translating/filtering packets on lo, can work
if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'pass on lo\n') pfctl('-f /dev/stdin', b'pass on lo\n')
super(Darwin, self).add_anchors() super(Darwin, self).add_anchors(anchor)
def _add_natlook_ports(self, pnl, src_port, dst_port): def _add_natlook_ports(self, pnl, src_port, dst_port):
pnl.sxport.port = socket.htons(src_port) pnl.sxport.port = socket.htons(src_port)
@ -314,21 +361,36 @@ class Darwin(FreeBsd):
return xport.port return xport.port
class PfSense(FreeBsd):
RULE_ACTION_OFFSET = 3040
def __init__(self):
self.pfioc_rule = c_char * 3112
super(PfSense, self).__init__()
if sys.platform == 'darwin': if sys.platform == 'darwin':
pf = Darwin() pf = Darwin()
elif sys.platform.startswith('openbsd'): elif sys.platform.startswith('openbsd'):
pf = OpenBsd() pf = OpenBsd()
elif platform.version().endswith('pfSense'):
pf = PfSense()
else: else:
pf = FreeBsd() pf = FreeBsd()
def pfctl(args, stdin=None): def pfctl(args, stdin=None):
argv = ['pfctl'] + list(args.split(" ")) argv = ['pfctl'] + shlex.split(args)
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE, p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
stdout=ssubprocess.PIPE, stdout=ssubprocess.PIPE,
stderr=ssubprocess.PIPE) stderr=ssubprocess.PIPE,
env=env)
o = p.communicate(stdin) o = p.communicate(stdin)
if p.returncode: if p.returncode:
raise Fatal('%r returned %d' % (argv, p.returncode)) raise Fatal('%r returned %d' % (argv, p.returncode))
@ -344,9 +406,17 @@ def pf_get_dev():
return _pf_fd return _pf_fd
def pf_get_anchor(family, port):
return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port)
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
@ -367,44 +437,41 @@ 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, reverse=True):
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"")))
pf.add_anchors() anchor = pf_get_anchor(family, port)
pf.add_rules(includes, port, dnsport, nslist) pf.add_anchors(anchor)
pf.add_rules(anchor, includes, port, dnsport, nslist, family)
pf.enable() pf.enable()
def restore_firewall(self, port, family, udp): def restore_firewall(self, port, family, udp, user):
if family != socket.AF_INET: if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception( raise Exception(
'Address family "%s" unsupported by pf method_name' 'Address family "%s" unsupported by pf method_name'
% 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))
pf.disable()
def firewall_command(self, line): def firewall_command(self, line):
if line.startswith('QUERY_PF_NAT '): if line.startswith('QUERY_PF_NAT '):

View File

@ -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
@ -63,7 +64,7 @@ if recvmsg == "python":
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
@ -149,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'
@ -163,12 +165,17 @@ 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)
@ -187,7 +194,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')
@ -197,33 +204,44 @@ 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),
@ -231,10 +249,10 @@ class Method(BaseMethod):
_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'

View File

@ -4,41 +4,8 @@ from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__ from sshuttle import __version__
# 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 # Subnet file, supporting empty lines and hash-started comment lines
def parse_subnet_file(s): def parse_subnetport_file(s):
try: try:
handle = open(s, 'r') handle = open(s, 'r')
except OSError: except OSError:
@ -46,57 +13,76 @@ def parse_subnet_file(s):
raw_config_lines = handle.readlines() raw_config_lines = handle.readlines()
subnets = [] subnets = []
for line_no, line in enumerate(raw_config_lines): for _, line in enumerate(raw_config_lines):
line = line.strip() line = line.strip()
if len(line) == 0: if not line:
continue continue
if line[0] == '#': if line[0] == '#':
continue continue
subnets.append(parse_subnet(line)) subnets.append(parse_subnetport(line))
return subnets return subnets
# 1.2.3.4/5 or just 1.2.3.4 # 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 or just 1:2::3 # [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
def parse_subnet(subnet_str): # example.com:123 or just example.com
if ':' in subnet_str: def parse_subnetport(s):
return parse_subnet6(subnet_str) if s.count(':') > 1:
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
else: else:
return parse_subnet4(subnet_str) 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:
addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
except socket.gaierror:
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))
# 1.2.3.4:567 or just 1.2.3.4 or just 567 # 1.2.3.4:567 or just 1.2.3.4 or just 567
def parse_ipport4(s): # [1:2::3]:456 or [1:2::3] or just [::]:567
# example.com:123 or just example.com
def parse_ipport(s):
s = str(s) s = str(s)
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', 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: if not m:
raise Fatal('%r is not a valid IP:port format' % s) 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), ip, port = m.groups()
int(port or 0)) ip = ip or '0.0.0.0'
if a > 255 or b > 255 or c > 255 or d > 255: port = int(port or 0)
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
if port > 65535: try:
raise Fatal('*:%d is greater than the maximum of 65535' % port) addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
if a is None: except socket.gaierror:
a = b = c = d = 0 raise Fatal('%r is not a valid IP:port format' % s)
return ('%d.%d.%d.%d' % (a, b, c, d), port)
family, _, _, _, addr = min(addrinfo)
return (family,) + addr[:2]
# [1:2::3]:456 or [1:2::3] or 456 def parse_list(lst):
def parse_ipport6(s): return re.split(r'[\s,]+', lst.strip()) if lst else []
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 []
class Concat(Action): class Concat(Action):
@ -106,19 +92,20 @@ class Concat(Action):
super(Concat, self).__init__(option_strings, dest, **kwargs) super(Concat, self).__init__(option_strings, dest, **kwargs)
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
curr_value = getattr(namespace, self.dest, []) curr_value = getattr(namespace, self.dest, None) or []
setattr(namespace, self.dest, curr_value + values) setattr(namespace, self.dest, curr_value + values)
parser = ArgumentParser( parser = ArgumentParser(
prog="sshuttle", prog="sshuttle",
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>" usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
fromfile_prefix_chars="@"
) )
parser.add_argument( parser.add_argument(
"subnets", "subnets",
metavar="IP/MASK [IP/MASK...]", metavar="IP/MASK[:PORT[-PORT]]...",
nargs="*", nargs="*",
type=parse_subnet, type=parse_subnetport,
help=""" help="""
capture and forward traffic to these subnets (whitespace separated) capture and forward traffic to these subnets (whitespace separated)
""" """
@ -134,7 +121,8 @@ parser.add_argument(
"-H", "--auto-hosts", "-H", "--auto-hosts",
action="store_true", action="store_true",
help=""" help="""
scan for remote hostnames and update local /etc/hosts continuously scan for remote hostnames and update local /etc/hosts as
they are found
""" """
) )
parser.add_argument( parser.add_argument(
@ -160,9 +148,19 @@ parser.add_argument(
capture and forward DNS requests made to the following servers 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.
"""
)
parser.add_argument( parser.add_argument(
"--method", "--method",
choices=["auto", "nat", "tproxy", "pf"], choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
metavar="TYPE", metavar="TYPE",
default="auto", default="auto",
help=""" help="""
@ -185,10 +183,10 @@ parser.add_argument(
) )
parser.add_argument( parser.add_argument(
"-x", "--exclude", "-x", "--exclude",
metavar="IP/MASK", metavar="IP/MASK[:PORT[-PORT]]",
action="append", action="append",
default=[parse_subnet('127.0.0.1/8')], default=[],
type=parse_subnet, type=parse_subnetport,
help=""" help="""
exclude this subnet (can be used more than once) exclude this subnet (can be used more than once)
""" """
@ -198,7 +196,7 @@ parser.add_argument(
metavar="PATH", metavar="PATH",
action=Concat, action=Concat,
dest="exclude", dest="exclude",
type=parse_subnet_file, type=parse_subnetport_file,
help=""" help="""
exclude the subnets in a file (whitespace separated) exclude the subnets in a file (whitespace separated)
""" """
@ -232,7 +230,8 @@ parser.add_argument(
metavar="HOSTNAME[,HOSTNAME]", metavar="HOSTNAME[,HOSTNAME]",
default=[], default=[],
help=""" help="""
with -H, use these hostnames for initial scan (comma-separated) comma-separated list of hostnames for initial scan (may be used with
or without --auto-hosts)
""" """
) )
parser.add_argument( parser.add_argument(
@ -269,8 +268,9 @@ parser.add_argument(
"-s", "--subnets", "-s", "--subnets",
metavar="PATH", metavar="PATH",
action=Concat, action=Concat,
dest="subnets", dest="subnets_file",
type=parse_subnet_file, default=[],
type=parse_subnetport_file,
help=""" help="""
file where the subnets are stored, instead of on the command line file where the subnets are stored, instead of on the command line
""" """
@ -290,6 +290,12 @@ parser.add_argument(
pidfile name (only if using --daemon) [%(default)s] 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( parser.add_argument(
"--firewall", "--firewall",
action="store_true", action="store_true",

40
sshuttle/sdnotify.py Normal file
View File

@ -0,0 +1,40 @@
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')

View File

@ -15,6 +15,11 @@ from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \ from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
resolvconf_random_nameserver resolvconf_random_nameserver
try:
from shutil import which
except ImportError:
from distutils.spawn import find_executable as which
def _ipmatch(ipstr): def _ipmatch(ipstr):
# FIXME: IPv4 only # FIXME: IPv4 only
@ -60,18 +65,39 @@ def _shl(n, bits):
return n * int(2 ** bits) return n * int(2 ** bits)
def _list_routes(): def _route_netstat(line):
# FIXME: IPv4 only cols = line.split(None)
argv = ['netstat', '-rn'] if len(cols) < 3:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) return None, None
routes = []
for line in p.stdout:
cols = re.split(r'\s+', line.decode("ASCII"))
ipw = _ipmatch(cols[0]) ipw = _ipmatch(cols[0])
if not ipw:
continue # some lines won't be parseable; never mind
maskw = _ipmatch(cols[2]) # linux only maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null 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
env = {
'PATH': os.environ['PATH'],
'LC_ALL': "C",
}
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env)
routes = []
for line in p.stdout:
if not line.strip():
continue
ipw, mask = extract_route(line.decode("ASCII"))
if not ipw:
continue
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(
@ -80,11 +106,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)
@ -94,7 +129,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:
@ -106,7 +141,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
@ -125,7 +160,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
@ -133,22 +168,43 @@ 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)
@ -219,11 +275,11 @@ class UdpProxy(Handler):
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):
debug1('Starting server with Python version %s\n' debug1('Starting server with Python version %s\n'
% platform.python_version()) % platform.python_version())
@ -273,7 +329,8 @@ def main(latency_control):
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
@ -281,6 +338,12 @@ def main(latency_control):
def new_channel(channel, data): def new_channel(channel, data):
(family, dstip, dstport) = data.decode("ASCII").split(',', 2) (family, dstip, dstport) = data.decode("ASCII").split(',', 2)
family = int(family) family = int(family)
# AF_INET is the same constant on Linux and BSD but AF_INET6
# 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))
@ -290,7 +353,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
@ -300,7 +363,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]

View File

@ -5,6 +5,7 @@ import socket
import zlib import zlib
import imp import imp
import subprocess as ssubprocess import subprocess as ssubprocess
import shlex
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
from sshuttle.helpers import debug2 from sshuttle.helpers import debug2
@ -15,6 +16,7 @@ except ImportError:
# Python 2.x # Python 2.x
from pipes import quote from pipes import quote
def readfile(name): def readfile(name):
tokens = name.split(".") tokens = name.split(".")
f = None f = None
@ -61,20 +63,20 @@ def empackage(z, name, data=None):
def connect(ssh_cmd, rhostport, python, stderr, options): def connect(ssh_cmd, rhostport, python, stderr, options):
portl = [] portl = []
if (rhostport or '').count(':') > 1: if re.sub(r'.*@', '', rhostport or '').count(':') > 1:
if rhostport.count(']') or rhostport.count('['): if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']') result = rhostport.split(']')
rhost = result[0].strip('[') rhost = result[0].strip('[')
if len(result) > 1: if len(result) > 1:
result[1] = result[1].strip(':') result[1] = result[1].strip(':')
if result[1] is not '': if result[1] != '':
portl = ['-p', str(int(result[1]))] portl = ['-p', str(int(result[1]))]
# can't disambiguate IPv6 colons and a port number. pass the hostname # can't disambiguate IPv6 colons and a port number. pass the hostname
# through. # through.
else: else:
rhost = rhostport rhost = rhostport
else: # IPv4 else: # IPv4
l = (rhostport or '').split(':', 1) l = (rhostport or '').rsplit(':', 1)
rhost = l[0] rhost = l[0]
if len(l) > 1: if len(l) > 1:
portl = ['-p', str(int(l[1]))] portl = ['-p', str(int(l[1]))]
@ -105,16 +107,16 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
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 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>/dev/null || P=python; "
"exec \"$P\" -c %s") % quote(pyscript) "exec \"$P\" -c %s") % quote(pyscript)
pycmd = ("exec /bin/sh -c %s" % quote(pycmd)) pycmd = ("exec /bin/sh -c %s" % quote(pycmd))
argv = (sshl + argv = (sshl +

View File

@ -54,7 +54,8 @@ 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]
def _add(l, elem): def _add(l, elem):
@ -199,7 +200,8 @@ class SockWrapper:
_, e = sys.exc_info()[:2] _, 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):
@ -269,7 +271,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:
@ -348,7 +350,7 @@ class Mux(Handler):
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
@ -357,8 +359,8 @@ 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):
@ -375,7 +377,8 @@ class Mux(Handler):
def send(self, channel, cmd, data): def send(self, channel, cmd, data):
assert isinstance(data, binary_type) assert isinstance(data, binary_type)
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)),
@ -476,7 +479,7 @@ class Mux(Handler):
_add(w, self.wsock) _add(w, self.wsock)
def callback(self, sock): def callback(self, sock):
(r, w, x) = select.select([self.rsock], [self.wsock], [], 0) (r, w, _) = select.select([self.rsock], [self.wsock], [], 0)
if self.rsock in r: if self.rsock in r:
self.handle() self.handle()
if self.outbuf and self.wsock in w: if self.outbuf and self.wsock in w:
@ -501,17 +504,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):
@ -540,9 +551,11 @@ class MuxWrapper(SockWrapper):
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:

View File

@ -1,22 +1,23 @@
from mock import Mock, patch, call from mock import Mock, patch, call
import io import io
from socket import AF_INET, AF_INET6
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,36 @@ 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 +117,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),
] ]

View File

@ -2,6 +2,8 @@ from mock import patch, call
import sys import sys
import io import io
import socket import socket
from socket import AF_INET, AF_INET6
import errno
import sshuttle.helpers import sshuttle.helpers
@ -132,10 +134,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,34 +159,40 @@ 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): if sys.version_info < (3, 0):
expected = "1" expected = "1"
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1" assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1"

View File

@ -1,6 +1,7 @@
import pytest import pytest
from mock import Mock, patch, call from mock import Mock, patch, call
import socket import socket
from socket import AF_INET, AF_INET6
import struct import struct
from sshuttle.helpers import Fatal from sshuttle.helpers import Fatal
@ -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,49 @@ 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') '--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()

View File

@ -1,6 +1,7 @@
import pytest import pytest
from mock import Mock, patch, call, ANY from mock import Mock, patch, call, ANY
import socket import socket
from socket import AF_INET, AF_INET6
from sshuttle.methods import get_method from sshuttle.methods import get_method
from sshuttle.helpers import Fatal from sshuttle.helpers import Fatal
@ -10,7 +11,7 @@ from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd
def test_get_supported_features(): def test_get_supported_features():
method = get_method('pf') method = get_method('pf')
features = method.get_supported_features() features = method.get_supported_features()
assert not features.ipv6 assert features.ipv6
assert not features.udp assert not features.udp
assert features.dns assert features.dns
@ -20,7 +21,7 @@ def test_get_tcp_dstip():
sock = Mock() sock = Mock()
sock.getpeername.return_value = ("127.0.0.1", 1024) sock.getpeername.return_value = ("127.0.0.1", 1024)
sock.getsockname.return_value = ("127.0.0.2", 1025) sock.getsockname.return_value = ("127.0.0.2", 1025)
sock.family = socket.AF_INET sock.family = AF_INET
firewall = Mock() firewall = Mock()
firewall.pfile.readline.return_value = \ firewall.pfile.readline.return_value = \
@ -94,7 +95,7 @@ def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
assert not method.firewall_command("somthing") assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
socket.AF_INET, socket.IPPROTO_TCP, AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024) "127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command) assert method.firewall_command(command)
@ -117,7 +118,7 @@ def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
assert not method.firewall_command("somthing") assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
socket.AF_INET, socket.IPPROTO_TCP, AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024) "127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command) assert method.firewall_command(command)
@ -140,7 +141,7 @@ def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
assert not method.firewall_command("somthing") assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
socket.AF_INET, socket.IPPROTO_TCP, AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024) "127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command) assert method.firewall_command(command)
@ -155,6 +156,8 @@ def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout):
def pfctl(args, stdin=None): def pfctl(args, stdin=None):
if args == '-s Interfaces -i lo -v':
return (b'lo0 (skip)',)
if args == '-s all': if args == '-s all':
return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n', return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n',
b'little lamb\n') b'little lamb\n')
@ -174,38 +177,16 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
method = get_method('pf') method = get_method('pf')
assert method.name == 'pf' assert method.name == 'pf'
with pytest.raises(Exception) as excinfo: # IPV6
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::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True) False,
assert str(excinfo.value) \ None)
== '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 == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
@ -215,17 +196,69 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
] ]
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
call('-f /dev/stdin', b'pass on lo\n'), call('-f /dev/stdin', b'pass on lo\n'),
call('-s all'), call('-s all'),
call('-a sshuttle -f /dev/stdin', call('-a sshuttle6-1024 -f /dev/stdin',
b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n' 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 quick inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
b'pass out route-to lo0 inet6 proto tcp to '
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
b'pass out 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'table <dns_servers> {1.2.3.33}\n'
b'rdr pass on lo0 proto tcp ' b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 to 1.2.3.0/24 '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n' b'-> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 proto udp ' b'rdr pass on lo0 inet proto udp '
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n' b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
b'pass out route-to lo0 inet proto tcp ' b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'to <forward_subnets> keep state\n' b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
b'pass out route-to lo0 inet proto udp ' b'pass out route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'), b'to <dns_servers> port 53 keep state\n'),
call('-E'), call('-E'),
@ -234,10 +267,10 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock() mock_ioctl.reset_mock()
mock_pfctl.reset_mock() mock_pfctl.reset_mock()
method.restore_firewall(1025, 2, False) method.restore_firewall(1025, AF_INET, False, None)
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-a sshuttle -F all'), call('-a sshuttle-1025 -F all'),
call("-X abcdefg"), call("-X abcdefg"),
] ]
mock_pf_get_dev.reset_mock() mock_pf_get_dev.reset_mock()
@ -247,36 +280,56 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
@patch('sshuttle.helpers.verbose', new=3) @patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.methods.pf.pf', FreeBsd()) @patch('sshuttle.methods.pf.pf', FreeBsd())
@patch('subprocess.call')
@patch('sshuttle.methods.pf.pfctl') @patch('sshuttle.methods.pf.pfctl')
@patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev') @patch('sshuttle.methods.pf.pf_get_dev')
def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl,
mock_subprocess_call):
mock_pfctl.side_effect = pfctl mock_pfctl.side_effect = pfctl
method = get_method('pf') method = get_method('pf')
assert method.name == 'pf' assert method.name == 'pf'
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::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True) False,
assert str(excinfo.value) \ None)
== 'Address family "AF_INET6" unsupported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_pfctl.mock_calls == [
assert mock_ioctl.mock_calls == [] call('-s all'),
assert mock_pfctl.mock_calls == [] 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 quick inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
b'pass out route-to lo0 inet6 proto tcp to '
b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
b'pass out 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: 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', 0, 0),
True) (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 str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
@ -284,10 +337,12 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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', 0, 0),
False) (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY),
@ -298,15 +353,14 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
] ]
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s all'), call('-s all'),
call('-a sshuttle -f /dev/stdin', call('-a sshuttle-1025 -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'table <dns_servers> {1.2.3.33}\n'
b'rdr pass on lo0 proto tcp ' b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n' b'to 1.2.3.0/24 -> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 proto udp ' b'rdr pass on lo0 inet proto udp '
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n' b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
b'pass out route-to lo0 inet proto tcp ' b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'to <forward_subnets> keep state\n' b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
b'pass out route-to lo0 inet proto udp ' b'pass out route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'), b'to <dns_servers> port 53 keep state\n'),
call('-e'), call('-e'),
@ -315,10 +369,12 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock() mock_ioctl.reset_mock()
mock_pfctl.reset_mock() mock_pfctl.reset_mock()
method.restore_firewall(1025, 2, False) method.restore_firewall(1025, AF_INET, False, None)
method.restore_firewall(1024, AF_INET6, False, None)
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-a sshuttle -F all'), call('-a sshuttle-1025 -F all'),
call('-a sshuttle6-1024 -F all'),
call("-d"), call("-d"),
] ]
mock_pf_get_dev.reset_mock() mock_pf_get_dev.reset_mock()
@ -337,27 +393,50 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
method = get_method('pf') method = get_method('pf')
assert method.name == 'pf' assert method.name == 'pf'
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::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True) False,
assert str(excinfo.value) \ None)
== 'Address family "AF_INET6" unsupported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [
assert mock_ioctl.mock_calls == [] call(mock_pf_get_dev(), 0xcd48441a, ANY),
assert mock_pfctl.mock_calls == [] call(mock_pf_get_dev(), 0xcd48441a, 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 quick inet6 proto tcp to '
b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
b'pass out inet6 proto tcp to 2404:6800:4004:80c::/64 '
b'port 8000:9000 route-to lo0 keep state\n'
b'pass out inet6 proto 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: 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', 0, 0),
True) (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 str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
@ -365,25 +444,28 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
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', 0, 0),
False) (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
False,
None)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd48441a, ANY), call(mock_pf_get_dev(), 0xcd48441a, ANY),
call(mock_pf_get_dev(), 0xcd48441a, ANY), call(mock_pf_get_dev(), 0xcd48441a, ANY),
] ]
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s Interfaces -i lo -v'),
call('-f /dev/stdin', b'match on lo\n'), call('-f /dev/stdin', b'match on lo\n'),
call('-s all'), call('-s all'),
call('-a sshuttle -f /dev/stdin', call('-a sshuttle-1025 -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'table <dns_servers> {1.2.3.33}\n'
b'pass in on lo0 inet proto tcp divert-to 127.0.0.1 port 1025\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'pass in on lo0 inet proto udp to '
b'<dns_servers> port 53 rdr-to 127.0.0.1 port 1027\n' b'<dns_servers> port 53 rdr-to 127.0.0.1 port 1027\n'
b'pass out inet proto tcp to ' b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'<forward_subnets> route-to lo0 keep state\n' b'pass out inet proto tcp to 1.2.3.0/24 route-to lo0 keep state\n'
b'pass out inet proto udp to ' b'pass out inet proto udp to '
b'<dns_servers> port 53 route-to lo0 keep state\n'), b'<dns_servers> port 53 route-to lo0 keep state\n'),
call('-e'), call('-e'),
@ -392,11 +474,13 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_ioctl.reset_mock() mock_ioctl.reset_mock()
mock_pfctl.reset_mock() mock_pfctl.reset_mock()
method.restore_firewall(1025, 2, False) method.restore_firewall(1025, AF_INET, False, None)
method.restore_firewall(1024, AF_INET6, False, None)
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-a sshuttle -F all'), call('-a sshuttle-1025 -F all'),
call("-d"), call('-a sshuttle6-1024 -F all'),
call('-d'),
] ]
mock_pf_get_dev.reset_mock() mock_pf_get_dev.reset_mock()
mock_pfctl.reset_mock() mock_pfctl.reset_mock()

View File

@ -1,3 +1,6 @@
import socket
from socket import AF_INET, AF_INET6
from mock import Mock, patch, call from mock import Mock, patch, call
from sshuttle.methods import get_method from sshuttle.methods import get_method
@ -49,7 +52,7 @@ def test_send_udp(mock_socket):
assert sock.mock_calls == [] assert sock.mock_calls == []
assert mock_socket.mock_calls == [ assert mock_socket.mock_calls == [
call(sock.family, 2), call(sock.family, 2),
call().setsockopt(1, 2, 1), call().setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),
call().setsockopt(0, 19, 1), call().setsockopt(0, 19, 1),
call().bind('127.0.0.2'), call().bind('127.0.0.2'),
call().sendto("2222222", '127.0.0.1'), call().sendto("2222222", '127.0.0.1'),
@ -100,93 +103,97 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
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::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True) True,
None)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(10, 'mangle', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', 'sshuttle-m-1024'),
call(10, 'mangle', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', 'sshuttle-t-1024'),
call(10, 'mangle', 'sshuttle-d-1024') call(AF_INET6, 'mangle', 'sshuttle-d-1024')
] ]
assert mock_ipt_ttl.mock_calls == [] assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
call(10, 'mangle', '-F', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
call(10, 'mangle', '-X', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1024'),
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'),
call(10, 'mangle', '-F', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'),
call(10, 'mangle', '-X', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1024'),
call(10, 'mangle', '-F', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'),
call(10, 'mangle', '-X', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1024'),
call(10, 'mangle', '-N', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-N', 'sshuttle-m-1024'),
call(10, 'mangle', '-F', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'),
call(10, 'mangle', '-N', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-N', 'sshuttle-d-1024'),
call(10, 'mangle', '-F', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'),
call(10, 'mangle', '-N', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-N', 'sshuttle-t-1024'),
call(10, 'mangle', '-F', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'),
call(10, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
call(10, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j',
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK', 'sshuttle-t-1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
'--set-mark', '1'), '--set-mark', '1'),
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'), call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'), '-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'), '-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'), '-m', 'udp', '-p', 'udp', '--dport', '53'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--tproxy-mark', '0x1/0x1',
'--dest', u'2404:6800:4004:80c::33/32', '--dest', u'2404:6800:4004:80c::33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--on-port', '1024'), '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', '--on-port', '1024'),
call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--on-port', '1024') '-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
'--on-port', '1024')
] ]
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, 10, True) method.restore_firewall(1025, AF_INET6, True, None)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(10, 'mangle', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', 'sshuttle-m-1025'),
call(10, 'mangle', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', 'sshuttle-t-1025'),
call(10, 'mangle', 'sshuttle-d-1025') call(AF_INET6, 'mangle', 'sshuttle-d-1025')
] ]
assert mock_ipt_ttl.mock_calls == [] assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [ assert mock_ipt.mock_calls == [
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(10, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'),
call(10, 'mangle', '-X', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1025'),
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(10, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1025'),
call(10, 'mangle', '-X', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1025'),
call(10, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1025'),
call(10, 'mangle', '-X', 'sshuttle-d-1025') call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()
@ -196,64 +203,71 @@ 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', 0, 0),
True) (AF_INET, 32, True, u'1.2.3.66', 80, 80)],
True,
None)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(2, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-m-1025'),
call(2, 'mangle', 'sshuttle-t-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'),
call(2, 'mangle', 'sshuttle-d-1025') call(AF_INET, 'mangle', 'sshuttle-d-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, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(2, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
call(2, 'mangle', '-X', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'),
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
call(2, 'mangle', '-X', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
call(2, 'mangle', '-X', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025'),
call(2, 'mangle', '-N', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-N', 'sshuttle-m-1025'),
call(2, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
call(2, 'mangle', '-N', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-N', 'sshuttle-d-1025'),
call(2, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
call(2, 'mangle', '-N', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-N', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
call(2, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
call(2, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j',
call(2, 'mangle', '-A', 'sshuttle-d-1025', 'sshuttle-t-1025'),
call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025',
'-j', 'MARK', '--set-mark', '1'), '-j', 'MARK', '--set-mark', '1'),
call(2, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'), call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'), '-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'), '-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.33/32', '--set-mark', '1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53'), '-m', 'udp', '-p', 'udp', '--dport', '53'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32', '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'), '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'), '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', '--dport', '80:80'),
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'), '--dport', '80:80'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'), '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', '--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', '--set-mark', '1', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'), '-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24', '--set-mark', '1', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
'-m', 'udp', '-p', 'udp', '--on-port', '1025') '-m', 'udp', '-p', 'udp', '--on-port', '1025')
] ]
@ -261,22 +275,22 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
method.restore_firewall(1025, 2, True) method.restore_firewall(1025, AF_INET, True, None)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(2, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-m-1025'),
call(2, 'mangle', 'sshuttle-t-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'),
call(2, 'mangle', 'sshuttle-d-1025') call(AF_INET, 'mangle', 'sshuttle-d-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, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
call(2, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'),
call(2, 'mangle', '-X', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'),
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'),
call(2, 'mangle', '-X', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'),
call(2, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'),
call(2, 'mangle', '-X', 'sshuttle-d-1025') call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()

View File

@ -0,0 +1,98 @@
import socket
import pytest
import sshuttle.options
from argparse import ArgumentTypeError as Fatal
_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)

View File

@ -0,0 +1,64 @@
from mock import Mock, patch, call
import socket
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),
]

View File

@ -1,7 +1,7 @@
import io import io
import socket import socket
import sshuttle.server import sshuttle.server
from mock import patch, Mock, call from mock import patch, Mock
def test__ipmatch(): def test__ipmatch():
@ -20,32 +20,9 @@ def test__maskbits():
sshuttle.server._maskbits(netmask) sshuttle.server._maskbits(netmask)
@patch('sshuttle.server.which', side_effect=lambda x: x == 'netstat')
@patch('sshuttle.server.ssubprocess.Popen') @patch('sshuttle.server.ssubprocess.Popen')
def test__listroutes(mock_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 mock_popen.mock_calls == [
call(['netstat', '-rn'], stdout=-1),
call().wait()
]
assert routes == [
(socket.AF_INET, '0.0.0.0', 0),
(socket.AF_INET, '192.168.1.0', 24)
]
@patch('sshuttle.server.ssubprocess.Popen')
def test_listroutes(mock_popen):
mock_pobj = Mock() mock_pobj = Mock()
mock_pobj.stdout = io.BytesIO(b""" mock_pobj.stdout = io.BytesIO(b"""
Kernel IP routing table Kernel IP routing table
@ -61,3 +38,21 @@ Destination Gateway Genmask Flags MSS Window irtt Iface
assert list(routes) == [ assert list(routes) == [
(socket.AF_INET, '192.168.1.0', 24) (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)
]

View File

@ -1,10 +1,10 @@
[tox] [tox]
downloadcache = {toxworkdir}/cache/ downloadcache = {toxworkdir}/cache/
envlist = envlist =
py26,
py27, py27,
py34, py34,
py35, py35,
py36,
[testenv] [testenv]
basepython = basepython =
@ -12,9 +12,10 @@ basepython =
py27: python2.7 py27: python2.7
py34: python3.4 py34: python3.4
py35: python3.5 py35: python3.5
py36: python3.6
commands = commands =
flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics
flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
py.test py.test
deps = deps =
pytest -rrequirements-tests.txt
mock
setuptools>=17.1