mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-07-04 08:40:30 +02:00
Compare commits
75 Commits
sshuttle-0
...
v0.73
Author | SHA1 | Date | |
---|---|---|---|
780997e8a7 | |||
d41579c265 | |||
974f9aee81 | |||
4252e81fb0 | |||
7e10fc0756 | |||
2c2ee12e58 | |||
256ed7d244 | |||
151634cd8c | |||
c0748c2388 | |||
71d46d77bf | |||
c1083e983f | |||
9944b97629 | |||
eaad54f68b | |||
6ebf76a5d8 | |||
51eb7862c4 | |||
75b6865a1d | |||
e3a1c56e54 | |||
99050aacb3 | |||
021e6f57af | |||
9cc6d63684 | |||
43566ebda6 | |||
537899c1df | |||
641a193d3d | |||
71d17e449e | |||
9d443e4155 | |||
cf0aaa7134 | |||
54de23aae3 | |||
ac723694bf | |||
bcd3205db1 | |||
a651d748cd | |||
fe48c7c026 | |||
4bd6ec8f01 | |||
ba1cf58a6c | |||
dd8e68b6dc | |||
1f2117917f | |||
e6f2395dac | |||
d4f10b232a | |||
41b8ad4c97 | |||
a82224c141 | |||
0fb714893a | |||
28be71ef9a | |||
d2ee34d71c | |||
3cf5002b62 | |||
f71704f54d | |||
ad83059da8 | |||
d211fc28ee | |||
f4dac68dc0 | |||
3a73520310 | |||
e127aab776 | |||
5f90ee1f04 | |||
d70b5f2b89 | |||
3f2de26f67 | |||
53d5260f8f | |||
f870ceba00 | |||
a38963301e | |||
bbd54e150d | |||
00f20657e3 | |||
84b30be904 | |||
5825dddb02 | |||
9eced8d049 | |||
fecb53413d | |||
1b1ed4d495 | |||
b19272a67a | |||
bc2a0b7fbc | |||
6a96ace497 | |||
163aab2ca1 | |||
964977220e | |||
db67834164 | |||
1bc2f84d16 | |||
a229fc020c | |||
d6e7a9b6ad | |||
e6ca7148fa | |||
95529a5137 | |||
93c4af6fc8 | |||
2ca9aaa450 |
11
.travis.yml
Normal file
11
.travis.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- 2.7
|
||||||
|
- 3.5
|
||||||
|
- pypy
|
||||||
|
|
||||||
|
install:
|
||||||
|
- travis_retry pip install -q pytest mock
|
||||||
|
|
||||||
|
script:
|
||||||
|
- PYTHONPATH=. py.test
|
5
MANIFEST.in
Normal file
5
MANIFEST.in
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
include *.txt
|
||||||
|
include *.rst
|
||||||
|
include *.py
|
||||||
|
include MANIFEST.in
|
||||||
|
recursive-include sshuttle *.py
|
215
README.md
215
README.md
@ -1,215 +0,0 @@
|
|||||||
|
|
||||||
WARNING:
|
|
||||||
On MacOS 10.6 (at least up to 10.6.6), your network will
|
|
||||||
stop responding about 10 minutes after the first time you
|
|
||||||
start sshuttle, because of a MacOS kernel bug relating to
|
|
||||||
arp and the net.inet.ip.scopedroute sysctl. To fix it,
|
|
||||||
just switch your wireless off and on. Sshuttle makes the
|
|
||||||
kernel setting it changes permanent, so this won't happen
|
|
||||||
again, even after a reboot.
|
|
||||||
|
|
||||||
Required Software
|
|
||||||
=================
|
|
||||||
|
|
||||||
- You need PyXAPI, available here:
|
|
||||||
http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
|
|
||||||
- You also need autossh, available in various package management systems
|
|
||||||
- Python 2.x, both locally and the remote system
|
|
||||||
|
|
||||||
|
|
||||||
sshuttle: where transparent proxy meets VPN meets ssh
|
|
||||||
=====================================================
|
|
||||||
|
|
||||||
As far as I know, sshuttle is the only program that solves the following
|
|
||||||
common case:
|
|
||||||
|
|
||||||
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
|
|
||||||
|
|
||||||
- You have access to a remote network via ssh.
|
|
||||||
|
|
||||||
- You don't necessarily have admin access on the remote network.
|
|
||||||
|
|
||||||
- The remote network has no VPN, or only stupid/complex VPN
|
|
||||||
protocols (IPsec, PPTP, etc). Or maybe you <i>are</i> the
|
|
||||||
admin and you just got frustrated with the awful state of
|
|
||||||
VPN tools.
|
|
||||||
|
|
||||||
- You don't want to create an ssh port forward for every
|
|
||||||
single host/port on the remote network.
|
|
||||||
|
|
||||||
- You hate openssh's port forwarding because it's randomly
|
|
||||||
slow and/or stupid.
|
|
||||||
|
|
||||||
- You can't use openssh's PermitTunnel feature because
|
|
||||||
it's disabled by default on openssh servers; plus it does
|
|
||||||
TCP-over-TCP, which has terrible performance (see below).
|
|
||||||
|
|
||||||
|
|
||||||
Prerequisites
|
|
||||||
-------------
|
|
||||||
|
|
||||||
- sudo, su, or logged in as root on your client machine.
|
|
||||||
(The server doesn't need admin access.)
|
|
||||||
|
|
||||||
- If you use Linux on your client machine:
|
|
||||||
iptables installed on the client, including at
|
|
||||||
least the iptables DNAT, REDIRECT, and ttl modules.
|
|
||||||
These are installed by default on most Linux distributions.
|
|
||||||
(The server doesn't need iptables and doesn't need to be
|
|
||||||
Linux.)
|
|
||||||
|
|
||||||
- If you use MacOS or BSD on your client machine:
|
|
||||||
Your kernel needs to be compiled with `IPFIREWALL_FORWARD`
|
|
||||||
(MacOS has this by default) and you need to have ipfw
|
|
||||||
available. (The server doesn't need to be MacOS or BSD.)
|
|
||||||
|
|
||||||
|
|
||||||
Obtaining sshuttle
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- First, go get PyXAPI from the link above
|
|
||||||
|
|
||||||
- Clone: `git clone https://github.com/sshuttle/sshuttle.git`
|
|
||||||
|
|
||||||
|
|
||||||
Usage on (Ubuntu) Linux
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
- `cd packaging; ./make_deb`
|
|
||||||
|
|
||||||
- `sudo dpkg -i ./sshuttle-VERSION.deb`
|
|
||||||
|
|
||||||
- Check out the files in `/etc/sshuttle`; configure them so your tunnel works
|
|
||||||
|
|
||||||
- `sudo service sshuttle start`
|
|
||||||
|
|
||||||
|
|
||||||
Usage on other Linuxes and OSes
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
<tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
|
|
||||||
|
|
||||||
- There is a shortcut for 0.0.0.0/0 for those that value
|
|
||||||
their wrists
|
|
||||||
<tt>./sshuttle -r username@sshserver 0/0 -vv</tt>
|
|
||||||
|
|
||||||
- If you would also like your DNS queries to be proxied
|
|
||||||
through the DNS server of the server you are connect to:
|
|
||||||
<tt>./sshuttle --dns -vvr username@sshserver 0/0</tt>
|
|
||||||
|
|
||||||
The above is probably what you want to use to prevent
|
|
||||||
local network attacks such as Firesheep and friends.
|
|
||||||
|
|
||||||
(You may be prompted for one or more passwords; first, the
|
|
||||||
local password to become root using either sudo or su, and
|
|
||||||
then the remote ssh password. Or you might have sudo and ssh set
|
|
||||||
up to not require passwords, in which case you won't be
|
|
||||||
prompted at all.)
|
|
||||||
|
|
||||||
Usage Notes
|
|
||||||
-----------
|
|
||||||
|
|
||||||
That's it! Now your local machine can access the remote network as if you
|
|
||||||
were right there. And if your "client" machine is a router, everyone on
|
|
||||||
your local network can make connections to your remote network.
|
|
||||||
|
|
||||||
You don't need to install sshuttle on the remote server;
|
|
||||||
the remote server just needs to have python available.
|
|
||||||
sshuttle will automatically upload and run its source code
|
|
||||||
to the remote python interpreter.
|
|
||||||
|
|
||||||
This creates a transparent proxy server on your local machine for all IP
|
|
||||||
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
|
|
||||||
you want; use any number of IP addresses or subnets to change which
|
|
||||||
addresses get proxied. Using 0.0.0.0/0 proxies <i>everything</i>, which is
|
|
||||||
interesting if you don't trust the people on your local network.)
|
|
||||||
|
|
||||||
Any TCP session you initiate to one of the proxied IP addresses will be
|
|
||||||
captured by sshuttle and sent over an ssh session to the remote copy of
|
|
||||||
sshuttle, which will then regenerate the connection on that end, and funnel
|
|
||||||
the data back and forth through ssh.
|
|
||||||
|
|
||||||
Fun, right? A poor man's instant VPN, and you don't even have to have
|
|
||||||
admin access on the server.
|
|
||||||
|
|
||||||
|
|
||||||
Theory of Operation
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind
|
|
||||||
of both, and kind of neither.
|
|
||||||
|
|
||||||
It's like a VPN, since it can forward every port on an entire network, not
|
|
||||||
just ports you specify. Conveniently, it lets you use the "real" IP
|
|
||||||
addresses of each host rather than faking port numbers on localhost.
|
|
||||||
|
|
||||||
On the other hand, the way it *works* is more like ssh port forwarding than
|
|
||||||
a VPN. Normally, a VPN forwards your data one packet at a time, and
|
|
||||||
doesn't care about individual connections; ie. it's "stateless" with respect
|
|
||||||
to the traffic. sshuttle is the opposite of stateless; it tracks every
|
|
||||||
single connection.
|
|
||||||
|
|
||||||
You could compare sshuttle to something like the old <a
|
|
||||||
href="http://en.wikipedia.org/wiki/Slirp">Slirp</a> program, which was a
|
|
||||||
userspace TCP/IP implementation that did something similar. But it
|
|
||||||
operated on a packet-by-packet basis on the client side, reassembling the
|
|
||||||
packets on the server side. That worked okay back in the "real live serial
|
|
||||||
port" days, because serial ports had predictable latency and buffering.
|
|
||||||
|
|
||||||
But you can't safely just forward TCP packets over a TCP session (like ssh),
|
|
||||||
because TCP's performance depends fundamentally on packet loss; it
|
|
||||||
<i>must</i> experience packet loss in order to know when to slow down! At
|
|
||||||
the same time, the outer TCP session (ssh, in this case) is a reliable
|
|
||||||
transport, which means that what you forward through the tunnel <i>never</i>
|
|
||||||
experiences packet loss. The ssh session itself experiences packet loss, of
|
|
||||||
course, but TCP fixes it up and ssh (and thus you) never know the
|
|
||||||
difference. But neither does your inner TCP session, and extremely screwy
|
|
||||||
performance ensues.
|
|
||||||
|
|
||||||
sshuttle assembles the TCP stream locally, multiplexes it statefully over
|
|
||||||
an ssh session, and disassembles it back into packets at the other end. So
|
|
||||||
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
|
||||||
safe.
|
|
||||||
|
|
||||||
|
|
||||||
Useless Trivia
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Back in 1998 (12 years ago! Yikes!), I released the first version of <a
|
|
||||||
href="http://alumnit.ca/wiki/?TunnelVisionReadMe">Tunnel Vision</a>, a
|
|
||||||
semi-intelligent VPN client for Linux. Unfortunately, I made two big mistakes:
|
|
||||||
I implemented the key exchange myself (oops), and I ended up doing
|
|
||||||
TCP-over-TCP (double oops). The resulting program worked okay - and people
|
|
||||||
used it for years - but the performance was always a bit funny. And nobody
|
|
||||||
ever found any security flaws in my key exchange, either, but that doesn't
|
|
||||||
mean anything. :)
|
|
||||||
|
|
||||||
The same year, dcoombs and I also released Fast Forward, a proxy server
|
|
||||||
supporting transparent proxying. Among other things, we used it for
|
|
||||||
automatically splitting traffic across more than one Internet connection (a
|
|
||||||
tool we called "Double Vision").
|
|
||||||
|
|
||||||
I was still in university at the time. A couple years after that, one of my
|
|
||||||
professors was working with some graduate students on the technology that
|
|
||||||
would eventually become <a href="http://www.slipstream.com/">Slipstream
|
|
||||||
Internet Acceleration</a>. He asked me to do a contract for him to build an
|
|
||||||
initial prototype of a transparent proxy server for mobile networks. The
|
|
||||||
idea was similar to sshuttle: if you reassemble and then disassemble the TCP
|
|
||||||
packets, you can reduce latency and improve performance vs. just forwarding
|
|
||||||
the packets over a plain VPN or mobile network. (It's unlikely that any of
|
|
||||||
my code has persisted in the Slipstream product today, but the concept is
|
|
||||||
still pretty cool. I'm still horrified that people use plain TCP on
|
|
||||||
complex mobile networks with crazily variable latency, for which it was
|
|
||||||
never really intended.)
|
|
||||||
|
|
||||||
That project I did for Slipstream was what first gave me the idea to merge
|
|
||||||
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
|
||||||
program that was the best of all worlds. And here we are, at last, 10 years
|
|
||||||
later. You're welcome.
|
|
||||||
|
|
||||||
--
|
|
||||||
Avery Pennarun <apenwarr@gmail.com>
|
|
||||||
|
|
||||||
Mailing list:
|
|
||||||
Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
|
|
||||||
List archives are at: http://groups.google.com/group/sshuttle
|
|
258
README.rst
Normal file
258
README.rst
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
sshuttle: where transparent proxy meets VPN meets ssh
|
||||||
|
=====================================================
|
||||||
|
|
||||||
|
As far as I know, sshuttle is the only program that solves the following
|
||||||
|
common case:
|
||||||
|
|
||||||
|
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
|
||||||
|
|
||||||
|
- You have access to a remote network via ssh.
|
||||||
|
|
||||||
|
- You don't necessarily have admin access on the remote network.
|
||||||
|
|
||||||
|
- The remote network has no VPN, or only stupid/complex VPN
|
||||||
|
protocols (IPsec, PPTP, etc). Or maybe you *are* the
|
||||||
|
admin and you just got frustrated with the awful state of
|
||||||
|
VPN tools.
|
||||||
|
|
||||||
|
- You don't want to create an ssh port forward for every
|
||||||
|
single host/port on the remote network.
|
||||||
|
|
||||||
|
- You hate openssh's port forwarding because it's randomly
|
||||||
|
slow and/or stupid.
|
||||||
|
|
||||||
|
- You can't use openssh's PermitTunnel feature because
|
||||||
|
it's disabled by default on openssh servers; plus it does
|
||||||
|
TCP-over-TCP, which has terrible performance (see below).
|
||||||
|
|
||||||
|
|
||||||
|
Client side Requirements
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- sudo, or logged in as root on your client machine.
|
||||||
|
(The server doesn't need admin access.)
|
||||||
|
- Python 2.7 or Python 3.5.
|
||||||
|
|
||||||
|
+-------+--------+------------+-----------------------------------------------+
|
||||||
|
| OS | Method | Features | Requirements |
|
||||||
|
+=======+========+============+===============================================+
|
||||||
|
| Linux | NAT | * IPv4 TCP + iptables DNAT, REDIRECT, and ttl modules. |
|
||||||
|
+ +--------+------------+-----------------------------------------------+
|
||||||
|
| | TPROXY | * IPv4 TCP + Linux with TPROXY support. |
|
||||||
|
| | | * IPv4 UDP + Python 3.5 preferred (see below). |
|
||||||
|
| | | * IPv6 TCP + |
|
||||||
|
| | | * IPv6 UDP + |
|
||||||
|
+-------+--------+------------+-----------------------------------------------+
|
||||||
|
| BSD | IPFW | * IPv4 TCP | Your kernel needs to be compiled with |
|
||||||
|
| | | | `IPFIREWALL_FORWARD` and you need to have ipfw|
|
||||||
|
| | | | available. |
|
||||||
|
+-------+--------+------------+-----------------------------------------------+
|
||||||
|
| MacOS | PF | * IPv4 TCP + You need to have the pfctl command. |
|
||||||
|
+-------+--------+------------+-----------------------------------------------+
|
||||||
|
|
||||||
|
The IPFW method is depreciated. It was originally required for MacOS support,
|
||||||
|
however is no longer maintained. It is likely to get removed from future
|
||||||
|
versions of sshuttle.
|
||||||
|
|
||||||
|
|
||||||
|
Server side Requirements
|
||||||
|
------------------------
|
||||||
|
Python 2.7 or Python 3.5. This should match what is used on the client side.
|
||||||
|
|
||||||
|
|
||||||
|
Additional Suggested Software
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
- You may want to use autossh, available in various package management
|
||||||
|
systems
|
||||||
|
|
||||||
|
|
||||||
|
Additional information for TPROXY
|
||||||
|
---------------------------------
|
||||||
|
TPROXY is the only method that supports full support of IPv6 and UDP.
|
||||||
|
|
||||||
|
Full UDP or DNS support with the TPROXY method requires the `recvmsg()`
|
||||||
|
syscall. This is not available in Python 2.7, however is in Python 3.5 and
|
||||||
|
later.
|
||||||
|
|
||||||
|
- For Python 2.7, you need PyXAPI, available here:
|
||||||
|
http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/
|
||||||
|
|
||||||
|
There are some things you need to consider for TPROXY to work:
|
||||||
|
|
||||||
|
1. The following commands need to be run first as root. This only needs to be
|
||||||
|
done once after booting up::
|
||||||
|
|
||||||
|
ip route add local default dev lo table 100
|
||||||
|
ip rule add fwmark 1 lookup 100
|
||||||
|
ip -6 route add local default dev lo table 100
|
||||||
|
ip -6 rule add fwmark 1 lookup 100
|
||||||
|
|
||||||
|
2. The client needs to be run as root. e.g.::
|
||||||
|
|
||||||
|
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
|
||||||
|
|
||||||
|
3. You do need the `--method=tproxy` parameter, as above.
|
||||||
|
|
||||||
|
4. The routes for the outgoing packets must already exist. For example, if your
|
||||||
|
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
|
||||||
|
packets will not be generated and sshuttle cannot intercept them. Add some
|
||||||
|
dummy routes to external interfaces. Make sure they get removed however
|
||||||
|
after sshuttle exits.
|
||||||
|
|
||||||
|
|
||||||
|
Obtaining sshuttle
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- From PyPI::
|
||||||
|
|
||||||
|
pip install sshuttle
|
||||||
|
|
||||||
|
- Clone::
|
||||||
|
|
||||||
|
git clone https://github.com/sshuttle/sshuttle.git
|
||||||
|
./setup.py install
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Forward all traffic::
|
||||||
|
|
||||||
|
sshuttle -r username@sshserver 0.0.0.0/0 -vv
|
||||||
|
|
||||||
|
- By default sshuttle will automatically choose a method to use. Override with
|
||||||
|
the `--method=` parameter.
|
||||||
|
|
||||||
|
- There is a shortcut for 0.0.0.0/0 for those that value
|
||||||
|
their wrists::
|
||||||
|
|
||||||
|
sshuttle -r username@sshserver 0/0 -vv
|
||||||
|
|
||||||
|
- If you would also like your DNS queries to be proxied
|
||||||
|
through the DNS server of the server you are connect to::
|
||||||
|
|
||||||
|
sshuttle --dns -vvr username@sshserver 0/0
|
||||||
|
|
||||||
|
The above is probably what you want to use to prevent
|
||||||
|
local network attacks such as Firesheep and friends.
|
||||||
|
|
||||||
|
(You may be prompted for one or more passwords; first, the local password to
|
||||||
|
become root using sudo, and then the remote ssh password. Or you might have
|
||||||
|
sudo and ssh set up to not require passwords, in which case you won't be
|
||||||
|
prompted at all.)
|
||||||
|
|
||||||
|
|
||||||
|
Usage Notes
|
||||||
|
-----------
|
||||||
|
|
||||||
|
That's it! Now your local machine can access the remote network as if you
|
||||||
|
were right there. And if your "client" machine is a router, everyone on
|
||||||
|
your local network can make connections to your remote network.
|
||||||
|
|
||||||
|
You don't need to install sshuttle on the remote server;
|
||||||
|
the remote server just needs to have python available.
|
||||||
|
sshuttle will automatically upload and run its source code
|
||||||
|
to the remote python interpreter.
|
||||||
|
|
||||||
|
This creates a transparent proxy server on your local machine for all IP
|
||||||
|
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
|
||||||
|
you want; use any number of IP addresses or subnets to change which
|
||||||
|
addresses get proxied. Using 0.0.0.0/0 proxies *everything*, which is
|
||||||
|
interesting if you don't trust the people on your local network.)
|
||||||
|
|
||||||
|
Any TCP session you initiate to one of the proxied IP addresses will be
|
||||||
|
captured by sshuttle and sent over an ssh session to the remote copy of
|
||||||
|
sshuttle, which will then regenerate the connection on that end, and funnel
|
||||||
|
the data back and forth through ssh.
|
||||||
|
|
||||||
|
Fun, right? A poor man's instant VPN, and you don't even have to have
|
||||||
|
admin access on the server.
|
||||||
|
|
||||||
|
|
||||||
|
Support
|
||||||
|
-------
|
||||||
|
|
||||||
|
Mailing list:
|
||||||
|
|
||||||
|
* Subscribe by sending a message to <sshuttle+subscribe@googlegroups.com>
|
||||||
|
* List archives are at: http://groups.google.com/group/sshuttle
|
||||||
|
|
||||||
|
Issue tracker and pull requests at github:
|
||||||
|
|
||||||
|
* https://github.com/sshuttle/sshuttle
|
||||||
|
|
||||||
|
|
||||||
|
Theory of Operation
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind
|
||||||
|
of both, and kind of neither.
|
||||||
|
|
||||||
|
It's like a VPN, since it can forward every port on an entire network, not
|
||||||
|
just ports you specify. Conveniently, it lets you use the "real" IP
|
||||||
|
addresses of each host rather than faking port numbers on localhost.
|
||||||
|
|
||||||
|
On the other hand, the way it *works* is more like ssh port forwarding than
|
||||||
|
a VPN. Normally, a VPN forwards your data one packet at a time, and
|
||||||
|
doesn't care about individual connections; ie. it's "stateless" with respect
|
||||||
|
to the traffic. sshuttle is the opposite of stateless; it tracks every
|
||||||
|
single connection.
|
||||||
|
|
||||||
|
You could compare sshuttle to something like the old `Slirp
|
||||||
|
<http://en.wikipedia.org/wiki/Slirp>`_ program, which was a userspace TCP/IP
|
||||||
|
implementation that did something similar. But it operated on a
|
||||||
|
packet-by-packet basis on the client side, reassembling the packets on the
|
||||||
|
server side. That worked okay back in the "real live serial port" days,
|
||||||
|
because serial ports had predictable latency and buffering.
|
||||||
|
|
||||||
|
But you can't safely just forward TCP packets over a TCP session (like ssh),
|
||||||
|
because TCP's performance depends fundamentally on packet loss; it
|
||||||
|
*must* experience packet loss in order to know when to slow down! At
|
||||||
|
the same time, the outer TCP session (ssh, in this case) is a reliable
|
||||||
|
transport, which means that what you forward through the tunnel *never*
|
||||||
|
experiences packet loss. The ssh session itself experiences packet loss, of
|
||||||
|
course, but TCP fixes it up and ssh (and thus you) never know the
|
||||||
|
difference. But neither does your inner TCP session, and extremely screwy
|
||||||
|
performance ensues.
|
||||||
|
|
||||||
|
sshuttle assembles the TCP stream locally, multiplexes it statefully over
|
||||||
|
an ssh session, and disassembles it back into packets at the other end. So
|
||||||
|
it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is
|
||||||
|
safe.
|
||||||
|
|
||||||
|
|
||||||
|
Useless Trivia
|
||||||
|
--------------
|
||||||
|
This section written by Avery Pennarun <apenwarr@gmail.com>.
|
||||||
|
|
||||||
|
Back in 1998 (12 years ago! Yikes!), I released the first version of `Tunnel
|
||||||
|
Vision <http://alumnit.ca/wiki/?TunnelVisionReadMe>`_, a semi-intelligent VPN
|
||||||
|
client for Linux. Unfortunately, I made two big mistakes: I implemented the
|
||||||
|
key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops).
|
||||||
|
The resulting program worked okay - and people used it for years - but the
|
||||||
|
performance was always a bit funny. And nobody ever found any security flaws
|
||||||
|
in my key exchange, either, but that doesn't mean anything. :)
|
||||||
|
|
||||||
|
The same year, dcoombs and I also released Fast Forward, a proxy server
|
||||||
|
supporting transparent proxying. Among other things, we used it for
|
||||||
|
automatically splitting traffic across more than one Internet connection (a
|
||||||
|
tool we called "Double Vision").
|
||||||
|
|
||||||
|
I was still in university at the time. A couple years after that, one of my
|
||||||
|
professors was working with some graduate students on the technology that would
|
||||||
|
eventually become `Slipstream Internet Acceleration
|
||||||
|
<http://www.slipstream.com/>`_. He asked me to do a contract for him to build
|
||||||
|
an initial prototype of a transparent proxy server for mobile networks. The
|
||||||
|
idea was similar to sshuttle: if you reassemble and then disassemble the TCP
|
||||||
|
packets, you can reduce latency and improve performance vs. just forwarding
|
||||||
|
the packets over a plain VPN or mobile network. (It's unlikely that any of my
|
||||||
|
code has persisted in the Slipstream product today, but the concept is still
|
||||||
|
pretty cool. I'm still horrified that people use plain TCP on complex mobile
|
||||||
|
networks with crazily variable latency, for which it was never really
|
||||||
|
intended.)
|
||||||
|
|
||||||
|
That project I did for Slipstream was what first gave me the idea to merge
|
||||||
|
the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single
|
||||||
|
program that was the best of all worlds. And here we are, at last, 10 years
|
||||||
|
later. You're welcome.
|
1
VERSION.txt
Normal file
1
VERSION.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.73
|
@ -1,26 +0,0 @@
|
|||||||
Package: sshuttle
|
|
||||||
Version: 0.2
|
|
||||||
Architecture: i386
|
|
||||||
Maintainer: Jim Wyllie <jwyllie83@gmail.com>
|
|
||||||
Depends: autossh, upstart, python (>=2.6)
|
|
||||||
Section: utils
|
|
||||||
Priority: optional
|
|
||||||
Homepage: http://github.com/jwyllie83/sshuttle.udp
|
|
||||||
Description: "Full-featured" VPN over an SSH tunnel, allowing full remote
|
|
||||||
access somewhere where all you have is an SSH connection. It works well if
|
|
||||||
you generally find yourself in the following situation:
|
|
||||||
.
|
|
||||||
- Your client machine (or router) is Linux, FreeBSD, or MacOS.
|
|
||||||
- You have access to a remote network via ssh.
|
|
||||||
- You don't necessarily have admin access on the remote network.
|
|
||||||
- You do not wish to, or can't, use other VPN software
|
|
||||||
- You don't want to create an ssh port forward for every
|
|
||||||
single host/port on the remote network.
|
|
||||||
- You hate openssh's port forwarding because it's randomly
|
|
||||||
slow and/or stupid.
|
|
||||||
- You can't use openssh's PermitTunnel feature because
|
|
||||||
it's disabled by default on openssh servers; plus it does
|
|
||||||
TCP-over-TCP, which has suboptimal performance
|
|
||||||
.
|
|
||||||
It also has hooks for more complicated setups (VPN-in-a-SSH-VPN, etc) to allow
|
|
||||||
you to set it up as you like.
|
|
@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# This script puts together a .deb package suitable for installing on an Ubuntu
|
|
||||||
# system
|
|
||||||
|
|
||||||
B="/tmp/sshuttle/build"
|
|
||||||
|
|
||||||
if [ ! -x /usr/bin/dpkg ]; then
|
|
||||||
echo 'Unable to build: dpkg not found on system'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the new directory structure
|
|
||||||
mkdir -p ${B}/etc/sshuttle/pre-start.d
|
|
||||||
mkdir -p ${B}/etc/sshuttle/post-stop.d
|
|
||||||
mkdir -p ${B}/usr/share/sshuttle
|
|
||||||
mkdir -p ${B}/usr/bin
|
|
||||||
mkdir -p ${B}/etc/init
|
|
||||||
mkdir -p ${B}/DEBIAN
|
|
||||||
|
|
||||||
# Copy over all of the files
|
|
||||||
cp -r ../src/* ${B}/usr/share/sshuttle
|
|
||||||
cp ../src/sshuttle ${B}/usr/bin
|
|
||||||
cp -r sshuttle.conf ${B}/etc/init
|
|
||||||
cp prefixes.conf ${B}/etc/sshuttle
|
|
||||||
cp tunnel.conf ${B}/etc/sshuttle
|
|
||||||
|
|
||||||
# Copy the control file over, as well
|
|
||||||
cp control ${B}/DEBIAN
|
|
||||||
|
|
||||||
# Create the md5sum manifest
|
|
||||||
if [ -x /usr/bin/md5sum ]; then
|
|
||||||
cd ${B}
|
|
||||||
find . -type f | egrep -v DEBIAN | sed -re 's/^..//' | xargs md5sum > ${B}/DEBIAN/md5sums
|
|
||||||
cd ${OLDPWD}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build the debian package
|
|
||||||
VERSION=$(egrep -e '^Version' control | sed -re 's/^[^:]*: //')
|
|
||||||
dpkg --build ${B} ./sshuttle-${VERSION}.deb
|
|
||||||
rm -rf ${B}
|
|
@ -1,5 +0,0 @@
|
|||||||
# Output prefixes here, one per line. Prefix is in:
|
|
||||||
# prefix/netmask format
|
|
||||||
# Like this:
|
|
||||||
# 192.168.0.0/16
|
|
||||||
# 192.0.43.10/32
|
|
@ -1,90 +0,0 @@
|
|||||||
description "Create a transparent proxy over SSH"
|
|
||||||
author "Jim Wyllie <jwyllie83@gmail.com>"
|
|
||||||
|
|
||||||
manual
|
|
||||||
nice -5
|
|
||||||
|
|
||||||
# Edit this file with network prefixes that should be loaded through the SSH
|
|
||||||
# tunnel.
|
|
||||||
env PREFIX_LOCATION=/etc/sshuttle/prefixes.conf
|
|
||||||
|
|
||||||
# Routing table; defaults to 100
|
|
||||||
env ROUTE_TABLE=100
|
|
||||||
|
|
||||||
# fwmark; defaults to 1
|
|
||||||
env FWMARK=1
|
|
||||||
|
|
||||||
# SSH tunnel configuration file
|
|
||||||
env SSHUTTLE_TUNNEL_FILE=/etc/sshuttle/tunnel.conf
|
|
||||||
|
|
||||||
# File containing the tunnel proxy name / host / whatever
|
|
||||||
env TUNNEL_PROXY="/etc/sshuttle/tunnel.conf"
|
|
||||||
|
|
||||||
# Any other commands needed to run before or after loading the SSH tunnel.
|
|
||||||
# This is where you can put any of your hacks to set up tunnels-in-tunnels,
|
|
||||||
# etc. Scripts in this directory are executed in order.
|
|
||||||
env MISC_START_DIR=/etc/sshuttle/pre-start.d
|
|
||||||
env MISC_STOP_DIR=/etc/sshuttle/post-stop.d
|
|
||||||
|
|
||||||
start on (local-filesystems and net-device-up IFACE!=lo)
|
|
||||||
stop on stopping network-services
|
|
||||||
|
|
||||||
#respawn
|
|
||||||
|
|
||||||
pre-start script
|
|
||||||
# Make sure we have created the routes
|
|
||||||
sudo ip rule add fwmark ${FWMARK} lookup ${ROUTE_TABLE}
|
|
||||||
logger "Starting sshuttle..."
|
|
||||||
|
|
||||||
if [ -f "${PREFIX_LOCATION}" ]; then
|
|
||||||
cat "${PREFIX_LOCATION}" | while read ROUTE; do
|
|
||||||
|
|
||||||
# Skip comments
|
|
||||||
if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Skip empty lines
|
|
||||||
if [ -z "${ROUTE}" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
logger "Adding route: ${ROUTE}"
|
|
||||||
ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE}
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
for RUNFILE in ${MISC_START_DIR}/*; do
|
|
||||||
logger "Executing ${RUNFILE}"
|
|
||||||
/bin/sh -c "${RUNFILE}"
|
|
||||||
done
|
|
||||||
end script
|
|
||||||
|
|
||||||
post-stop script
|
|
||||||
if [ -f "${PREFIX_LOCATION}" ]; then
|
|
||||||
cat "${PREFIX_LOCATION}" | while read ROUTE; do
|
|
||||||
|
|
||||||
# Skip comments
|
|
||||||
if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Skip empty lines
|
|
||||||
if [ -z "${ROUTE}" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
logger "Deleting route: ${ROUTE}"
|
|
||||||
ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE}
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
ip rule del fwmark ${FWMARK}
|
|
||||||
|
|
||||||
for RUNFILE in "${MISC_STOP_DIR}/*"; do
|
|
||||||
logger "Executing ${RUNFILE}"
|
|
||||||
/bin/sh -c "${RUNFILE}"
|
|
||||||
done
|
|
||||||
end script
|
|
||||||
|
|
||||||
exec /usr/bin/sshuttle --dns --method=tproxy --listen 0.0.0.0 --remote sshuttle_tunnel -s /etc/sshuttle/prefixes.conf -e "ssh -F ${TUNNEL_PROXY}"
|
|
@ -1,19 +0,0 @@
|
|||||||
# Here is where you can specify any SSH tunnel options See ssh_config(5) for
|
|
||||||
# details. You need to leave the Host line intact, but everything else can
|
|
||||||
# specify whatever you want
|
|
||||||
Host sshuttle_tunnel
|
|
||||||
|
|
||||||
# REQUIRED: Set this to be the host to which you would like to connect your
|
|
||||||
# tunnel
|
|
||||||
#Hostname localhost
|
|
||||||
|
|
||||||
# REQUIRED: Set this to be the target SSH user on the remote system
|
|
||||||
#User foo
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# The rest are all optional; see ssh_config(5) for the full list of what can
|
|
||||||
# be specified. Some very commonly needed ones are below.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# SSH key used for connecting
|
|
||||||
#IdentityFile /path/to/key
|
|
6
run
Executable file
6
run
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if python2 -V 2>/dev/null; then
|
||||||
|
exec python2 -m "sshuttle" "$@"
|
||||||
|
else
|
||||||
|
exec python -m "sshuttle" "$@"
|
||||||
|
fi
|
52
setup.py
Executable file
52
setup.py
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright 2012-2014 Brian May
|
||||||
|
#
|
||||||
|
# This file is part of python-tldap.
|
||||||
|
#
|
||||||
|
# python-tldap is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# python-tldap is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with python-tldap If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open('VERSION.txt', 'r') as f:
|
||||||
|
version = f.readline().strip()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="sshuttle",
|
||||||
|
version=version,
|
||||||
|
url='https://github.com/sshuttle/sshuttle',
|
||||||
|
author='Brian May',
|
||||||
|
author_email='brian@linuxpenguins.xyz',
|
||||||
|
description='Transparent proxy server that works as a poor man\'s VPN.',
|
||||||
|
packages=find_packages(),
|
||||||
|
license="GPL2+",
|
||||||
|
long_description=open('README.rst').read(),
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: End Users/Desktop",
|
||||||
|
"License :: OSI Approved :: "
|
||||||
|
"GNU General Public License v2 or later (GPLv2+)",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 2.7",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
|
"Topic :: System :: Networking",
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'sshuttle = sshuttle.__main__',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
keywords="ssh vpn",
|
||||||
|
)
|
10
src/Makefile
10
src/Makefile
@ -1,10 +0,0 @@
|
|||||||
all:
|
|
||||||
|
|
||||||
Makefile:
|
|
||||||
@
|
|
||||||
|
|
||||||
%: FORCE
|
|
||||||
+./do $@
|
|
||||||
|
|
||||||
.PHONY: FORCE
|
|
||||||
|
|
11
src/all.do
11
src/all.do
@ -1,11 +0,0 @@
|
|||||||
exec >&2
|
|
||||||
UI=
|
|
||||||
[ "$(uname)" = "Darwin" ] && UI=ui-macos/all
|
|
||||||
redo-ifchange sshuttle.8 $UI
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "What now?"
|
|
||||||
[ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app"
|
|
||||||
echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0"
|
|
||||||
echo "- Read the README: less README.md"
|
|
||||||
echo "- Read the man page: less sshuttle.md"
|
|
@ -1,27 +0,0 @@
|
|||||||
import sys
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
z = zlib.decompressobj()
|
|
||||||
mainmod = sys.modules[__name__]
|
|
||||||
while 1:
|
|
||||||
name = sys.stdin.readline().strip()
|
|
||||||
if name:
|
|
||||||
nbytes = int(sys.stdin.readline())
|
|
||||||
if verbosity >= 2:
|
|
||||||
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
|
||||||
% (name, nbytes))
|
|
||||||
content = z.decompress(sys.stdin.read(nbytes))
|
|
||||||
exec compile(content, name, "exec")
|
|
||||||
|
|
||||||
# FIXME: this crushes everything into a single module namespace,
|
|
||||||
# then makes each of the module names point at this one. Gross.
|
|
||||||
assert(name.endswith('.py'))
|
|
||||||
modname = name[:-3]
|
|
||||||
mainmod.__dict__[modname] = mainmod
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
verbose = verbosity
|
|
||||||
sys.stderr.flush()
|
|
||||||
sys.stdout.flush()
|
|
||||||
main()
|
|
@ -1,2 +0,0 @@
|
|||||||
redo ui-macos/clean
|
|
||||||
rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc
|
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
|||||||
exec >&2
|
|
||||||
if pandoc </dev/null 2>/dev/null; then
|
|
||||||
pandoc -s -r markdown -w man -o $3 $2.md
|
|
||||||
else
|
|
||||||
echo "Warning: pandoc not installed; can't generate manpages."
|
|
||||||
redo-always
|
|
||||||
fi
|
|
175
src/do
175
src/do
@ -1,175 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# A minimal alternative to djb redo that doesn't support incremental builds.
|
|
||||||
# For the full version, visit http://github.com/apenwarr/redo
|
|
||||||
#
|
|
||||||
# The author disclaims copyright to this source file and hereby places it in
|
|
||||||
# the public domain. (2010 12 14)
|
|
||||||
#
|
|
||||||
|
|
||||||
# By default, no output coloring.
|
|
||||||
green=""
|
|
||||||
bold=""
|
|
||||||
plain=""
|
|
||||||
|
|
||||||
if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
|
|
||||||
green="$(printf '\033[32m')"
|
|
||||||
bold="$(printf '\033[1m')"
|
|
||||||
plain="$(printf '\033[m')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_dirsplit()
|
|
||||||
{
|
|
||||||
base=${1##*/}
|
|
||||||
dir=${1%$base}
|
|
||||||
}
|
|
||||||
|
|
||||||
dirname()
|
|
||||||
(
|
|
||||||
_dirsplit "$1"
|
|
||||||
dir=${dir%/}
|
|
||||||
echo "${dir:-.}"
|
|
||||||
)
|
|
||||||
|
|
||||||
_dirsplit "$0"
|
|
||||||
export REDO=$(cd "${dir:-.}" && echo "$PWD/$base")
|
|
||||||
|
|
||||||
DO_TOP=
|
|
||||||
if [ -z "$DO_BUILT" ]; then
|
|
||||||
DO_TOP=1
|
|
||||||
[ -n "$*" ] || set all # only toplevel redo has a default target
|
|
||||||
export DO_BUILT=$PWD/.do_built
|
|
||||||
: >>"$DO_BUILT"
|
|
||||||
echo "Removing previously built files..." >&2
|
|
||||||
sort -u "$DO_BUILT" | tee "$DO_BUILT.new" |
|
|
||||||
while read f; do printf "%s\0%s.did\0" "$f" "$f"; done |
|
|
||||||
xargs -0 rm -f 2>/dev/null
|
|
||||||
mv "$DO_BUILT.new" "$DO_BUILT"
|
|
||||||
DO_PATH=$DO_BUILT.dir
|
|
||||||
export PATH=$DO_PATH:$PATH
|
|
||||||
rm -rf "$DO_PATH"
|
|
||||||
mkdir "$DO_PATH"
|
|
||||||
for d in redo redo-ifchange; do
|
|
||||||
ln -s "$REDO" "$DO_PATH/$d";
|
|
||||||
done
|
|
||||||
[ -e /bin/true ] && TRUE=/bin/true || TRUE=/usr/bin/true
|
|
||||||
for d in redo-ifcreate redo-stamp redo-always; do
|
|
||||||
ln -s $TRUE "$DO_PATH/$d";
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
_find_dofile_pwd()
|
|
||||||
{
|
|
||||||
dofile=default.$1.do
|
|
||||||
while :; do
|
|
||||||
dofile=default.${dofile#default.*.}
|
|
||||||
[ -e "$dofile" -o "$dofile" = default.do ] && break
|
|
||||||
done
|
|
||||||
ext=${dofile#default}
|
|
||||||
ext=${ext%.do}
|
|
||||||
base=${1%$ext}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_find_dofile()
|
|
||||||
{
|
|
||||||
local prefix=
|
|
||||||
while :; do
|
|
||||||
_find_dofile_pwd "$1"
|
|
||||||
[ -e "$dofile" ] && break
|
|
||||||
[ "$PWD" = "/" ] && break
|
|
||||||
target=${PWD##*/}/$target
|
|
||||||
tmp=${PWD##*/}/$tmp
|
|
||||||
prefix=${PWD##*/}/$prefix
|
|
||||||
cd ..
|
|
||||||
done
|
|
||||||
base=$prefix$base
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_run_dofile()
|
|
||||||
{
|
|
||||||
export DO_DEPTH="$DO_DEPTH "
|
|
||||||
export REDO_TARGET=$PWD/$target
|
|
||||||
local line1
|
|
||||||
set -e
|
|
||||||
read line1 <"$PWD/$dofile"
|
|
||||||
cmd=${line1#"#!/"}
|
|
||||||
if [ "$cmd" != "$line1" ]; then
|
|
||||||
/$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
|
|
||||||
else
|
|
||||||
:; . "$PWD/$dofile" >"$tmp.tmp2"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_do()
|
|
||||||
{
|
|
||||||
local dir=$1 target=$2 tmp=$3
|
|
||||||
if [ ! -e "$target" ] || [ -d "$target" -a ! -e "$target.did" ]; then
|
|
||||||
printf '%sdo %s%s%s%s\n' \
|
|
||||||
"$green" "$DO_DEPTH" "$bold" "$dir$target" "$plain" >&2
|
|
||||||
echo "$PWD/$target" >>"$DO_BUILT"
|
|
||||||
dofile=$target.do
|
|
||||||
base=$target
|
|
||||||
ext=
|
|
||||||
[ -e "$target.do" ] || _find_dofile "$target"
|
|
||||||
if [ ! -e "$dofile" ]; then
|
|
||||||
echo "do: $target: no .do file" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
[ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] ||
|
|
||||||
: >>"$target.did"
|
|
||||||
( _run_dofile "$target" "$base" "$tmp.tmp" )
|
|
||||||
rv=$?
|
|
||||||
if [ $rv != 0 ]; then
|
|
||||||
printf "do: %s%s\n" "$DO_DEPTH" \
|
|
||||||
"$dir$target: got exit code $rv" >&2
|
|
||||||
rm -f "$tmp.tmp" "$tmp.tmp2"
|
|
||||||
return $rv
|
|
||||||
fi
|
|
||||||
mv "$tmp.tmp" "$target" 2>/dev/null ||
|
|
||||||
! test -s "$tmp.tmp2" ||
|
|
||||||
mv "$tmp.tmp2" "$target" 2>/dev/null
|
|
||||||
rm -f "$tmp.tmp2"
|
|
||||||
else
|
|
||||||
echo "do $DO_DEPTH$target exists." >&2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Make corrections for directories that don't actually exist yet.
|
|
||||||
_dir_shovel()
|
|
||||||
{
|
|
||||||
local dir base
|
|
||||||
xdir=$1 xbase=$2 xbasetmp=$2
|
|
||||||
while [ ! -d "$xdir" -a -n "$xdir" ]; do
|
|
||||||
_dirsplit "${xdir%/}"
|
|
||||||
xbasetmp=${base}__$xbase
|
|
||||||
xdir=$dir xbase=$base/$xbase
|
|
||||||
echo "xbasetmp='$xbasetmp'" >&2
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
redo()
|
|
||||||
{
|
|
||||||
for i in "$@"; do
|
|
||||||
_dirsplit "$i"
|
|
||||||
_dir_shovel "$dir" "$base"
|
|
||||||
dir=$xdir base=$xbase basetmp=$xbasetmp
|
|
||||||
( cd "$dir" && _do "$dir" "$base" "$basetmp" ) || return 1
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
set -e
|
|
||||||
redo "$@"
|
|
||||||
|
|
||||||
if [ -n "$DO_TOP" ]; then
|
|
||||||
echo "Removing stamp files..." >&2
|
|
||||||
[ ! -e "$DO_BUILT" ] ||
|
|
||||||
while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" |
|
|
||||||
xargs -0 rm -f 2>/dev/null
|
|
||||||
fi
|
|
832
src/firewall.py
832
src/firewall.py
@ -1,832 +0,0 @@
|
|||||||
import errno
|
|
||||||
import socket
|
|
||||||
import select
|
|
||||||
import signal
|
|
||||||
import struct
|
|
||||||
import compat.ssubprocess as ssubprocess
|
|
||||||
import ssyslog
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from helpers import log, debug1, debug3, islocal, Fatal, family_to_string, \
|
|
||||||
resolvconf_nameservers
|
|
||||||
from fcntl import ioctl
|
|
||||||
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
|
||||||
sizeof, addressof, memmove
|
|
||||||
|
|
||||||
|
|
||||||
# python doesn't have a definition for this
|
|
||||||
IPPROTO_DIVERT = 254
|
|
||||||
|
|
||||||
|
|
||||||
def nonfatal(func, *args):
|
|
||||||
try:
|
|
||||||
func(*args)
|
|
||||||
except Fatal, e:
|
|
||||||
log('error: %s\n' % e)
|
|
||||||
|
|
||||||
|
|
||||||
def ipt_chain_exists(family, table, name):
|
|
||||||
if family == socket.AF_INET6:
|
|
||||||
cmd = 'ip6tables'
|
|
||||||
elif family == socket.AF_INET:
|
|
||||||
cmd = 'iptables'
|
|
||||||
else:
|
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
|
||||||
argv = [cmd, '-t', table, '-nL']
|
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
|
||||||
for line in p.stdout:
|
|
||||||
if line.startswith('Chain %s ' % name):
|
|
||||||
return True
|
|
||||||
rv = p.wait()
|
|
||||||
if rv:
|
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
|
||||||
|
|
||||||
|
|
||||||
def _ipt(family, table, *args):
|
|
||||||
if family == socket.AF_INET6:
|
|
||||||
argv = ['ip6tables', '-t', table] + list(args)
|
|
||||||
elif family == socket.AF_INET:
|
|
||||||
argv = ['iptables', '-t', table] + list(args)
|
|
||||||
else:
|
|
||||||
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
|
||||||
rv = ssubprocess.call(argv)
|
|
||||||
if rv:
|
|
||||||
raise Fatal('%r returned %d' % (argv, rv))
|
|
||||||
|
|
||||||
|
|
||||||
_no_ttl_module = False
|
|
||||||
|
|
||||||
|
|
||||||
def _ipt_ttl(family, *args):
|
|
||||||
global _no_ttl_module
|
|
||||||
if not _no_ttl_module:
|
|
||||||
# we avoid infinite loops by generating server-side connections
|
|
||||||
# with ttl 42. This makes the client side not recapture those
|
|
||||||
# connections, in case client == server.
|
|
||||||
try:
|
|
||||||
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
|
|
||||||
_ipt(family, *argsplus)
|
|
||||||
except Fatal:
|
|
||||||
_ipt(family, *args)
|
|
||||||
# we only get here if the non-ttl attempt succeeds
|
|
||||||
log('sshuttle: warning: your iptables is missing '
|
|
||||||
'the ttl module.\n')
|
|
||||||
_no_ttl_module = True
|
|
||||||
else:
|
|
||||||
_ipt(family, *args)
|
|
||||||
|
|
||||||
|
|
||||||
# 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 do_iptables_nat(port, dnsport, family, subnets, udp):
|
|
||||||
# only ipv4 supported with NAT
|
|
||||||
if family != socket.AF_INET:
|
|
||||||
raise Exception(
|
|
||||||
'Address family "%s" unsupported by nat method'
|
|
||||||
% family_to_string(family))
|
|
||||||
if udp:
|
|
||||||
raise Exception("UDP not supported by nat method")
|
|
||||||
|
|
||||||
table = "nat"
|
|
||||||
|
|
||||||
def ipt(*args):
|
|
||||||
return _ipt(family, table, *args)
|
|
||||||
|
|
||||||
def ipt_ttl(*args):
|
|
||||||
return _ipt_ttl(family, table, *args)
|
|
||||||
|
|
||||||
chain = 'sshuttle-%s' % port
|
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
|
||||||
if ipt_chain_exists(family, table, chain):
|
|
||||||
nonfatal(ipt, '-D', 'OUTPUT', '-j', chain)
|
|
||||||
nonfatal(ipt, '-D', 'PREROUTING', '-j', chain)
|
|
||||||
nonfatal(ipt, '-F', chain)
|
|
||||||
ipt('-X', chain)
|
|
||||||
|
|
||||||
if subnets or dnsport:
|
|
||||||
ipt('-N', chain)
|
|
||||||
ipt('-F', chain)
|
|
||||||
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
|
||||||
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
|
||||||
|
|
||||||
if subnets:
|
|
||||||
# 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:
|
|
||||||
ipt('-A', chain, '-j', 'RETURN',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-p', 'tcp')
|
|
||||||
else:
|
|
||||||
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-p', 'tcp',
|
|
||||||
'--to-ports', str(port))
|
|
||||||
|
|
||||||
if dnsport:
|
|
||||||
nslist = resolvconf_nameservers()
|
|
||||||
for f, ip in filter(lambda i: i[0] == family, nslist):
|
|
||||||
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
|
||||||
'--dest', '%s/32' % ip,
|
|
||||||
'-p', 'udp',
|
|
||||||
'--dport', '53',
|
|
||||||
'--to-ports', str(dnsport))
|
|
||||||
|
|
||||||
|
|
||||||
def do_iptables_tproxy(port, dnsport, family, subnets, udp):
|
|
||||||
if family not in [socket.AF_INET, socket.AF_INET6]:
|
|
||||||
raise Exception(
|
|
||||||
'Address family "%s" unsupported by tproxy method'
|
|
||||||
% family_to_string(family))
|
|
||||||
|
|
||||||
table = "mangle"
|
|
||||||
|
|
||||||
def ipt(*args):
|
|
||||||
return _ipt(family, table, *args)
|
|
||||||
|
|
||||||
def ipt_ttl(*args):
|
|
||||||
return _ipt_ttl(family, table, *args)
|
|
||||||
|
|
||||||
mark_chain = 'sshuttle-m-%s' % port
|
|
||||||
tproxy_chain = 'sshuttle-t-%s' % port
|
|
||||||
divert_chain = 'sshuttle-d-%s' % port
|
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
|
||||||
if ipt_chain_exists(family, table, mark_chain):
|
|
||||||
ipt('-D', 'OUTPUT', '-j', mark_chain)
|
|
||||||
ipt('-F', mark_chain)
|
|
||||||
ipt('-X', mark_chain)
|
|
||||||
|
|
||||||
if ipt_chain_exists(family, table, tproxy_chain):
|
|
||||||
ipt('-D', 'PREROUTING', '-j', tproxy_chain)
|
|
||||||
ipt('-F', tproxy_chain)
|
|
||||||
ipt('-X', tproxy_chain)
|
|
||||||
|
|
||||||
if ipt_chain_exists(family, table, divert_chain):
|
|
||||||
ipt('-F', divert_chain)
|
|
||||||
ipt('-X', divert_chain)
|
|
||||||
|
|
||||||
if subnets or dnsport:
|
|
||||||
ipt('-N', mark_chain)
|
|
||||||
ipt('-F', mark_chain)
|
|
||||||
ipt('-N', divert_chain)
|
|
||||||
ipt('-F', divert_chain)
|
|
||||||
ipt('-N', tproxy_chain)
|
|
||||||
ipt('-F', tproxy_chain)
|
|
||||||
ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
|
|
||||||
ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
|
|
||||||
ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
|
|
||||||
ipt('-A', divert_chain, '-j', 'ACCEPT')
|
|
||||||
ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
|
||||||
if subnets and udp:
|
|
||||||
ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
|
||||||
'-m', 'udp', '-p', 'udp')
|
|
||||||
|
|
||||||
if dnsport:
|
|
||||||
nslist = resolvconf_nameservers()
|
|
||||||
for f, ip in filter(lambda i: i[0] == family, nslist):
|
|
||||||
ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
|
||||||
'--dest', '%s/32' % ip,
|
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
|
||||||
ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1',
|
|
||||||
'--dest', '%s/32' % ip,
|
|
||||||
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
|
||||||
'--on-port', str(dnsport))
|
|
||||||
|
|
||||||
if subnets:
|
|
||||||
for f, swidth, sexclude, snet \
|
|
||||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
|
||||||
if sexclude:
|
|
||||||
ipt('-A', mark_chain, '-j', 'RETURN',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
|
||||||
ipt('-A', tproxy_chain, '-j', 'RETURN',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
|
||||||
else:
|
|
||||||
ipt('-A', mark_chain, '-j', 'MARK',
|
|
||||||
'--set-mark', '1',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'tcp', '-p', 'tcp')
|
|
||||||
ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'tcp', '-p', 'tcp',
|
|
||||||
'--on-port', str(port))
|
|
||||||
|
|
||||||
if sexclude and udp:
|
|
||||||
ipt('-A', mark_chain, '-j', 'RETURN',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'udp', '-p', 'udp')
|
|
||||||
ipt('-A', tproxy_chain, '-j', 'RETURN',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'udp', '-p', 'udp')
|
|
||||||
elif udp:
|
|
||||||
ipt('-A', mark_chain, '-j', 'MARK',
|
|
||||||
'--set-mark', '1',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'udp', '-p', 'udp')
|
|
||||||
ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
|
||||||
'--tproxy-mark', '0x1/0x1',
|
|
||||||
'--dest', '%s/%s' % (snet, swidth),
|
|
||||||
'-m', 'udp', '-p', 'udp',
|
|
||||||
'--on-port', str(port))
|
|
||||||
|
|
||||||
|
|
||||||
def ipfw_rule_exists(n):
|
|
||||||
argv = ['ipfw', 'list']
|
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
|
||||||
found = False
|
|
||||||
for line in p.stdout:
|
|
||||||
if line.startswith('%05d ' % n):
|
|
||||||
if not ('ipttl 42' in line
|
|
||||||
or ('skipto %d' % (n + 1)) 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]
|
|
||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
|
||||||
for line in p.stdout:
|
|
||||||
assert(line[-1] == '\n')
|
|
||||||
(k, v) = line[:-1].split(': ', 1)
|
|
||||||
_oldctls[k] = v
|
|
||||||
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 _udp_unpack(p):
|
|
||||||
src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0])
|
|
||||||
dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0])
|
|
||||||
return src, dst
|
|
||||||
|
|
||||||
|
|
||||||
def _udp_repack(p, src, dst):
|
|
||||||
addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0])
|
|
||||||
ports = struct.pack('!HH', src[1], dst[1])
|
|
||||||
return p[:12] + addrs + ports + p[24:]
|
|
||||||
|
|
||||||
|
|
||||||
_real_dns_server = [None]
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_diversion(divertsock, dnsport):
|
|
||||||
p, tag = divertsock.recvfrom(4096)
|
|
||||||
src, dst = _udp_unpack(p)
|
|
||||||
debug3('got diverted packet from %r to %r\n' % (src, dst))
|
|
||||||
if dst[1] == 53:
|
|
||||||
# outgoing DNS
|
|
||||||
debug3('...packet is a DNS request.\n')
|
|
||||||
_real_dns_server[0] = dst
|
|
||||||
dst = ('127.0.0.1', dnsport)
|
|
||||||
elif src[1] == dnsport:
|
|
||||||
if islocal(src[0]):
|
|
||||||
debug3('...packet is a DNS response.\n')
|
|
||||||
src = _real_dns_server[0]
|
|
||||||
else:
|
|
||||||
log('weird?! unexpected divert from %r to %r\n' % (src, dst))
|
|
||||||
assert(0)
|
|
||||||
newp = _udp_repack(p, src, dst)
|
|
||||||
divertsock.sendto(newp, tag)
|
|
||||||
|
|
||||||
|
|
||||||
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 do_ipfw(port, dnsport, family, subnets, udp):
|
|
||||||
# IPv6 not supported
|
|
||||||
if family not in [socket.AF_INET, ]:
|
|
||||||
raise Exception(
|
|
||||||
'Address family "%s" unsupported by ipfw method'
|
|
||||||
% family_to_string(family))
|
|
||||||
if udp:
|
|
||||||
raise Exception("UDP not supported by ipfw method")
|
|
||||||
|
|
||||||
sport = str(port)
|
|
||||||
xsport = str(port + 1)
|
|
||||||
|
|
||||||
# cleanup any existing rules
|
|
||||||
if ipfw_rule_exists(port):
|
|
||||||
ipfw('delete', sport)
|
|
||||||
|
|
||||||
while _changedctls:
|
|
||||||
name = _changedctls.pop()
|
|
||||||
oldval = _oldctls[name]
|
|
||||||
_sysctl_set(name, oldval)
|
|
||||||
|
|
||||||
if subnets or dnsport:
|
|
||||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
|
||||||
changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
|
|
||||||
if changed:
|
|
||||||
log("\n"
|
|
||||||
" WARNING: ONE-TIME NETWORK DISRUPTION:\n"
|
|
||||||
" =====================================\n"
|
|
||||||
"sshuttle has changed a MacOS kernel setting to work around\n"
|
|
||||||
"a bug in MacOS 10.6. This will cause your network to drop\n"
|
|
||||||
"within 5-10 minutes unless you restart your network\n"
|
|
||||||
"interface (change wireless networks or unplug/plug the\n"
|
|
||||||
"ethernet port) NOW, then restart sshuttle. The fix is\n"
|
|
||||||
"permanent; you only have to do this once.\n\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
ipfw('add', sport, 'check-state', 'ip',
|
|
||||||
'from', 'any', 'to', 'any')
|
|
||||||
|
|
||||||
if subnets:
|
|
||||||
# create new subnet entries
|
|
||||||
for f, swidth, sexclude, snet \
|
|
||||||
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
|
||||||
if sexclude:
|
|
||||||
ipfw('add', sport, 'skipto', xsport,
|
|
||||||
'tcp',
|
|
||||||
'from', 'any', 'to', '%s/%s' % (snet, swidth))
|
|
||||||
else:
|
|
||||||
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
|
||||||
'tcp',
|
|
||||||
'from', 'any', 'to', '%s/%s' % (snet, swidth),
|
|
||||||
'not', 'ipttl', '42', 'keep-state', 'setup')
|
|
||||||
|
|
||||||
# This part is much crazier than it is on Linux, because MacOS (at least
|
|
||||||
# 10.6, and probably other versions, and maybe FreeBSD too) doesn't
|
|
||||||
# correctly fixup the dstip/dstport for UDP packets when it puts them
|
|
||||||
# through a 'fwd' rule. It also doesn't fixup the srcip/srcport in the
|
|
||||||
# response packet. In Linux iptables, all that happens magically for us,
|
|
||||||
# so we just redirect the packets and relax.
|
|
||||||
#
|
|
||||||
# On MacOS, we have to fix the ports ourselves. For that, we use a
|
|
||||||
# 'divert' socket, which receives raw packets and lets us mangle them.
|
|
||||||
#
|
|
||||||
# Here's how it works. Let's say the local DNS server is 1.1.1.1:53,
|
|
||||||
# and the remote DNS server is 2.2.2.2:53, and the local transproxy port
|
|
||||||
# is 10.0.0.1:12300, and a client machine is making a request from
|
|
||||||
# 10.0.0.5:9999. We see a packet like this:
|
|
||||||
# 10.0.0.5:9999 -> 1.1.1.1:53
|
|
||||||
# Since the destip:port matches one of our local nameservers, it will
|
|
||||||
# match a 'fwd' rule, thus grabbing it on the local machine. However,
|
|
||||||
# the local kernel will then see a packet addressed to *:53 and
|
|
||||||
# not know what to do with it; there's nobody listening on port 53. Thus,
|
|
||||||
# we divert it, rewriting it into this:
|
|
||||||
# 10.0.0.5:9999 -> 10.0.0.1:12300
|
|
||||||
# This gets proxied out to the server, which sends it to 2.2.2.2:53,
|
|
||||||
# and the answer comes back, and the proxy sends it back out like this:
|
|
||||||
# 10.0.0.1:12300 -> 10.0.0.5:9999
|
|
||||||
# But that's wrong! The original machine expected an answer from
|
|
||||||
# 1.1.1.1:53, so we have to divert the *answer* and rewrite it:
|
|
||||||
# 1.1.1.1:53 -> 10.0.0.5:9999
|
|
||||||
#
|
|
||||||
# See? Easy stuff.
|
|
||||||
if dnsport:
|
|
||||||
divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
|
|
||||||
IPPROTO_DIVERT)
|
|
||||||
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
|
|
||||||
|
|
||||||
nslist = resolvconf_nameservers()
|
|
||||||
for f, ip in filter(lambda i: i[0] == family, nslist):
|
|
||||||
# relabel and then catch outgoing DNS requests
|
|
||||||
ipfw('add', sport, 'divert', sport,
|
|
||||||
'udp',
|
|
||||||
'from', 'any', 'to', '%s/32' % ip, '53',
|
|
||||||
'not', 'ipttl', '42')
|
|
||||||
# relabel DNS responses
|
|
||||||
ipfw('add', sport, 'divert', sport,
|
|
||||||
'udp',
|
|
||||||
'from', 'any', str(dnsport), 'to', 'any',
|
|
||||||
'not', 'ipttl', '42')
|
|
||||||
|
|
||||||
def do_wait():
|
|
||||||
while 1:
|
|
||||||
r, w, x = select.select([sys.stdin, divertsock], [], [])
|
|
||||||
if divertsock in r:
|
|
||||||
_handle_diversion(divertsock, dnsport)
|
|
||||||
if sys.stdin in r:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
do_wait = None
|
|
||||||
|
|
||||||
return do_wait
|
|
||||||
|
|
||||||
|
|
||||||
def pfctl(args, stdin = None):
|
|
||||||
argv = ['pfctl'] + list(args.split(" "))
|
|
||||||
debug1('>> %s\n' % ' '.join(argv))
|
|
||||||
|
|
||||||
p = ssubprocess.Popen(argv, stdin = ssubprocess.PIPE,
|
|
||||||
stdout = ssubprocess.PIPE,
|
|
||||||
stderr = ssubprocess.PIPE)
|
|
||||||
o = p.communicate(stdin)
|
|
||||||
if p.returncode:
|
|
||||||
raise Fatal('%r returned %d' % (argv, p.returncode))
|
|
||||||
|
|
||||||
return o
|
|
||||||
|
|
||||||
_pf_context = {'started_by_sshuttle': False, 'Xtoken':''}
|
|
||||||
|
|
||||||
def do_pf(port, dnsport, family, subnets, udp):
|
|
||||||
global _pf_started_by_sshuttle
|
|
||||||
tables = []
|
|
||||||
translating_rules = []
|
|
||||||
filtering_rules = []
|
|
||||||
|
|
||||||
if subnets:
|
|
||||||
include_subnets = filter(lambda s:not s[2], sorted(subnets, reverse=True))
|
|
||||||
if include_subnets:
|
|
||||||
tables.append('table <include_subnets> {%s}' % ','.join(["%s/%s" % (n[3], n[1]) for n in include_subnets]))
|
|
||||||
translating_rules.append('rdr pass on lo0 proto tcp to <include_subnets> -> 127.0.0.1 port %r' % port)
|
|
||||||
filtering_rules.append('pass out route-to lo0 inet proto tcp to <include_subnets> keep state')
|
|
||||||
|
|
||||||
exclude_subnets = filter(lambda s:s[2], sorted(subnets, reverse=True))
|
|
||||||
if exclude_subnets:
|
|
||||||
tables.append('table <exclude_subnets> {%s}' % ','.join(["%s/%s" % (n[3], n[1]) for n in exclude_subnets]))
|
|
||||||
filtering_rules.append('pass out route-to lo0 inet proto tcp to <exclude_subnets> keep state')
|
|
||||||
|
|
||||||
if dnsport:
|
|
||||||
nslist = resolvconf_nameservers()
|
|
||||||
tables.append('table <dns_servers> {%s}' % ','.join([ns[1] for ns in nslist]))
|
|
||||||
translating_rules.append('rdr pass on lo0 proto udp to <dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
|
|
||||||
filtering_rules.append('pass out route-to lo0 inet proto udp to <dns_servers> port 53 keep state')
|
|
||||||
|
|
||||||
rules = '\n'.join(tables + translating_rules + filtering_rules) + '\n'
|
|
||||||
|
|
||||||
pf_status = pfctl('-s all')[0]
|
|
||||||
if not '\nrdr-anchor "sshuttle" all\n' in pf_status:
|
|
||||||
pf_add_anchor_rule(PF_RDR, "sshuttle")
|
|
||||||
if not '\nanchor "sshuttle" all\n' in pf_status:
|
|
||||||
pf_add_anchor_rule(PF_PASS, "sshuttle")
|
|
||||||
|
|
||||||
pfctl('-a sshuttle -f /dev/stdin', rules)
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
o = pfctl('-E')
|
|
||||||
_pf_context['Xtoken'] = re.search(r'Token : (.+)', o[1]).group(1)
|
|
||||||
elif 'INFO:\nStatus: Disabled' in pf_status:
|
|
||||||
pfctl('-e')
|
|
||||||
_pf_context['started_by_sshuttle'] = True
|
|
||||||
else:
|
|
||||||
pfctl('-a sshuttle -F all')
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
pfctl('-X %s' % _pf_context['Xtoken'])
|
|
||||||
elif _pf_context['started_by_sshuttle']:
|
|
||||||
pfctl('-d')
|
|
||||||
|
|
||||||
|
|
||||||
def program_exists(name):
|
|
||||||
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
|
||||||
for p in paths:
|
|
||||||
fn = '%s/%s' % (p, name)
|
|
||||||
if os.path.exists(fn):
|
|
||||||
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
|
||||||
|
|
||||||
|
|
||||||
hostmap = {}
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_etc_hosts(port):
|
|
||||||
HOSTSFILE = '/etc/hosts'
|
|
||||||
BAKFILE = '%s.sbak' % HOSTSFILE
|
|
||||||
APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port
|
|
||||||
old_content = ''
|
|
||||||
st = None
|
|
||||||
try:
|
|
||||||
old_content = open(HOSTSFILE).read()
|
|
||||||
st = os.stat(HOSTSFILE)
|
|
||||||
except IOError, e:
|
|
||||||
if e.errno == errno.ENOENT:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
if old_content.strip() and not os.path.exists(BAKFILE):
|
|
||||||
os.link(HOSTSFILE, BAKFILE)
|
|
||||||
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
|
||||||
f = open(tmpname, 'w')
|
|
||||||
for line in old_content.rstrip().split('\n'):
|
|
||||||
if line.find(APPEND) >= 0:
|
|
||||||
continue
|
|
||||||
f.write('%s\n' % line)
|
|
||||||
for (name, ip) in sorted(hostmap.items()):
|
|
||||||
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
if st:
|
|
||||||
os.chown(tmpname, st.st_uid, st.st_gid)
|
|
||||||
os.chmod(tmpname, st.st_mode)
|
|
||||||
else:
|
|
||||||
os.chown(tmpname, 0, 0)
|
|
||||||
os.chmod(tmpname, 0644)
|
|
||||||
os.rename(tmpname, HOSTSFILE)
|
|
||||||
|
|
||||||
|
|
||||||
def restore_etc_hosts(port):
|
|
||||||
global hostmap
|
|
||||||
hostmap = {}
|
|
||||||
rewrite_etc_hosts(port)
|
|
||||||
|
|
||||||
|
|
||||||
# This are some classes and functions used to support pf in yosemite.
|
|
||||||
class pf_state_xport(Union):
|
|
||||||
_fields_ = [("port", c_uint16),
|
|
||||||
("call_id", c_uint16),
|
|
||||||
("spi", c_uint32)]
|
|
||||||
|
|
||||||
class pf_addr(Structure):
|
|
||||||
class _pfa(Union):
|
|
||||||
_fields_ = [("v4", c_uint32), # struct in_addr
|
|
||||||
("v6", c_uint32 * 4), # struct in6_addr
|
|
||||||
("addr8", c_uint8 * 16),
|
|
||||||
("addr16", c_uint16 * 8),
|
|
||||||
("addr32", c_uint32 * 4)]
|
|
||||||
|
|
||||||
_fields_ = [("pfa", _pfa)]
|
|
||||||
_anonymous_ = ("pfa",)
|
|
||||||
|
|
||||||
class pfioc_natlook(Structure):
|
|
||||||
_fields_ = [("saddr", pf_addr),
|
|
||||||
("daddr", pf_addr),
|
|
||||||
("rsaddr", pf_addr),
|
|
||||||
("rdaddr", pf_addr),
|
|
||||||
("sxport", pf_state_xport),
|
|
||||||
("dxport", pf_state_xport),
|
|
||||||
("rsxport", pf_state_xport),
|
|
||||||
("rdxport", pf_state_xport),
|
|
||||||
("af", c_uint8), # sa_family_t
|
|
||||||
("proto", c_uint8),
|
|
||||||
("proto_variant", c_uint8),
|
|
||||||
("direction", c_uint8)]
|
|
||||||
|
|
||||||
pfioc_rule = c_char * 3104 # sizeof(struct pfioc_rule)
|
|
||||||
|
|
||||||
pfioc_pooladdr = c_char * 1136 # sizeof(struct pfioc_pooladdr)
|
|
||||||
|
|
||||||
MAXPATHLEN = 1024
|
|
||||||
|
|
||||||
DIOCNATLOOK = ((0x40000000L | 0x80000000L) | ((sizeof(pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23))
|
|
||||||
DIOCCHANGERULE = ((0x40000000L | 0x80000000L) | ((sizeof(pfioc_rule) & 0x1fff) << 16) | ((ord('D')) << 8) | (26))
|
|
||||||
DIOCBEGINADDRS = ((0x40000000L | 0x80000000L) | ((sizeof(pfioc_pooladdr) & 0x1fff) << 16) | ((ord('D')) << 8) | (51))
|
|
||||||
|
|
||||||
PF_CHANGE_ADD_TAIL = 2
|
|
||||||
PF_CHANGE_GET_TICKET = 6
|
|
||||||
|
|
||||||
PF_PASS = 0
|
|
||||||
PF_RDR = 8
|
|
||||||
|
|
||||||
PF_OUT = 2
|
|
||||||
|
|
||||||
_pf_fd = None
|
|
||||||
|
|
||||||
def pf_get_dev():
|
|
||||||
global _pf_fd
|
|
||||||
if _pf_fd == None:
|
|
||||||
_pf_fd = os.open('/dev/pf', os.O_RDWR)
|
|
||||||
|
|
||||||
return _pf_fd
|
|
||||||
|
|
||||||
def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
|
|
||||||
[proto, family, src_port, dst_port] = [int(v) for v in [proto, family, src_port, dst_port]]
|
|
||||||
|
|
||||||
length = 4 if family == socket.AF_INET else 16
|
|
||||||
|
|
||||||
pnl = pfioc_natlook()
|
|
||||||
pnl.proto = proto
|
|
||||||
pnl.direction = PF_OUT
|
|
||||||
pnl.af = family
|
|
||||||
memmove(addressof(pnl.saddr), socket.inet_pton(pnl.af, src_ip), length)
|
|
||||||
pnl.sxport.port = socket.htons(src_port)
|
|
||||||
memmove(addressof(pnl.daddr), socket.inet_pton(pnl.af, dst_ip), length)
|
|
||||||
pnl.dxport.port = socket.htons(dst_port)
|
|
||||||
|
|
||||||
ioctl(pf_get_dev(), DIOCNATLOOK, (c_char * sizeof(pnl)).from_address(addressof(pnl)))
|
|
||||||
|
|
||||||
ip = socket.inet_ntop(pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)))
|
|
||||||
port = socket.ntohs(pnl.rdxport.port)
|
|
||||||
return (ip, port)
|
|
||||||
|
|
||||||
def pf_add_anchor_rule(type, name):
|
|
||||||
ACTION_OFFSET = 0
|
|
||||||
POOL_TICKET_OFFSET = 8
|
|
||||||
ANCHOR_CALL_OFFSET = 1040
|
|
||||||
RULE_ACTION_OFFSET = 3068
|
|
||||||
|
|
||||||
pr = pfioc_rule()
|
|
||||||
ppa = pfioc_pooladdr()
|
|
||||||
|
|
||||||
ioctl(pf_get_dev(), DIOCBEGINADDRS, ppa)
|
|
||||||
|
|
||||||
memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) #pool_ticket
|
|
||||||
memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name, min(MAXPATHLEN, len(name))) #anchor_call = name
|
|
||||||
memmove(addressof(pr) + RULE_ACTION_OFFSET, struct.pack('I', type), 4) #rule.action = type
|
|
||||||
|
|
||||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack('I', PF_CHANGE_GET_TICKET), 4) #action = PF_CHANGE_GET_TICKET
|
|
||||||
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
|
|
||||||
|
|
||||||
memmove(addressof(pr) + ACTION_OFFSET, struct.pack('I', PF_CHANGE_ADD_TAIL), 4) #action = PF_CHANGE_ADD_TAIL
|
|
||||||
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
|
|
||||||
|
|
||||||
|
|
||||||
# This is some voodoo for setting up the kernel's transparent
|
|
||||||
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
|
||||||
# otherwise we delete it, then make them from scratch.
|
|
||||||
#
|
|
||||||
# This code is supposed to clean up after itself by deleting its rules on
|
|
||||||
# exit. In case that fails, it's not the end of the world; future runs will
|
|
||||||
# supercede it in the transproxy list, at least, so the leftover rules
|
|
||||||
# are hopefully harmless.
|
|
||||||
def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
|
|
||||||
assert(port_v6 >= 0)
|
|
||||||
assert(port_v6 <= 65535)
|
|
||||||
assert(port_v4 >= 0)
|
|
||||||
assert(port_v4 <= 65535)
|
|
||||||
assert(dnsport_v6 >= 0)
|
|
||||||
assert(dnsport_v6 <= 65535)
|
|
||||||
assert(dnsport_v4 >= 0)
|
|
||||||
assert(dnsport_v4 <= 65535)
|
|
||||||
|
|
||||||
if os.getuid() != 0:
|
|
||||||
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
|
||||||
|
|
||||||
if method == "auto":
|
|
||||||
if program_exists('ipfw'):
|
|
||||||
method = "ipfw"
|
|
||||||
elif program_exists('iptables'):
|
|
||||||
method = "nat"
|
|
||||||
elif program_exists('pfctl'):
|
|
||||||
method = "pf"
|
|
||||||
else:
|
|
||||||
raise Fatal("can't find either ipfw, iptables or pfctl; check your PATH")
|
|
||||||
|
|
||||||
if method == "nat":
|
|
||||||
do_it = do_iptables_nat
|
|
||||||
elif method == "tproxy":
|
|
||||||
do_it = do_iptables_tproxy
|
|
||||||
elif method == "ipfw":
|
|
||||||
do_it = do_ipfw
|
|
||||||
elif method == "pf":
|
|
||||||
do_it = do_pf
|
|
||||||
else:
|
|
||||||
raise Exception('Unknown method "%s"' % method)
|
|
||||||
|
|
||||||
# because of limitations of the 'su' command, the *real* stdin/stdout
|
|
||||||
# are both attached to stdout initially. Clone stdout into stdin so we
|
|
||||||
# can read from it.
|
|
||||||
os.dup2(1, 0)
|
|
||||||
|
|
||||||
if syslog:
|
|
||||||
ssyslog.start_syslog()
|
|
||||||
ssyslog.stderr_to_syslog()
|
|
||||||
|
|
||||||
debug1('firewall manager ready method %s.\n' % method)
|
|
||||||
sys.stdout.write('READY %s\n' % method)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
# don't disappear if our controlling terminal or stdout/stderr
|
|
||||||
# disappears; we still have to clean up.
|
|
||||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
|
||||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
|
||||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
||||||
|
|
||||||
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
|
||||||
# I'll die automatically.
|
|
||||||
os.setsid()
|
|
||||||
|
|
||||||
# we wait until we get some input before creating the rules. That way,
|
|
||||||
# sshuttle can launch us as early as possible (and get sudo password
|
|
||||||
# authentication as early in the startup process as possible).
|
|
||||||
line = sys.stdin.readline(128)
|
|
||||||
if not line:
|
|
||||||
return # parent died; nothing to do
|
|
||||||
|
|
||||||
subnets = []
|
|
||||||
if line != 'ROUTES\n':
|
|
||||||
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
|
||||||
while 1:
|
|
||||||
line = sys.stdin.readline(128)
|
|
||||||
if not line:
|
|
||||||
raise Fatal('firewall: expected route but got %r' % line)
|
|
||||||
elif line == 'GO\n':
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
(family, width, exclude, ip) = line.strip().split(',', 3)
|
|
||||||
except:
|
|
||||||
raise Fatal('firewall: expected route or GO but got %r' % line)
|
|
||||||
subnets.append((int(family), int(width), bool(int(exclude)), ip))
|
|
||||||
|
|
||||||
try:
|
|
||||||
if line:
|
|
||||||
debug1('firewall manager: starting transproxy.\n')
|
|
||||||
|
|
||||||
subnets_v6 = filter(lambda i: i[0] == socket.AF_INET6, subnets)
|
|
||||||
if port_v6:
|
|
||||||
do_wait = do_it(
|
|
||||||
port_v6, dnsport_v6, socket.AF_INET6, subnets_v6, udp)
|
|
||||||
elif len(subnets_v6) > 0:
|
|
||||||
debug1("IPv6 subnets defined but IPv6 disabled\n")
|
|
||||||
|
|
||||||
subnets_v4 = filter(lambda i: i[0] == socket.AF_INET, subnets)
|
|
||||||
if port_v4:
|
|
||||||
do_wait = do_it(
|
|
||||||
port_v4, dnsport_v4, socket.AF_INET, subnets_v4, udp)
|
|
||||||
elif len(subnets_v4) > 0:
|
|
||||||
debug1('IPv4 subnets defined but IPv4 disabled\n')
|
|
||||||
|
|
||||||
sys.stdout.write('STARTED\n')
|
|
||||||
|
|
||||||
try:
|
|
||||||
sys.stdout.flush()
|
|
||||||
except IOError:
|
|
||||||
# the parent process died for some reason; he's surely been loud
|
|
||||||
# enough, so no reason to report another error
|
|
||||||
return
|
|
||||||
|
|
||||||
# Now we wait until EOF or any other kind of exception. We need
|
|
||||||
# to stay running so that we don't need a *second* password
|
|
||||||
# authentication at shutdown time - that cleanup is important!
|
|
||||||
while 1:
|
|
||||||
if do_wait:
|
|
||||||
do_wait()
|
|
||||||
line = sys.stdin.readline(128)
|
|
||||||
if line.startswith('HOST '):
|
|
||||||
(name, ip) = line[5:].strip().split(',', 1)
|
|
||||||
hostmap[name] = ip
|
|
||||||
rewrite_etc_hosts(port_v6 or port_v4)
|
|
||||||
elif line.startswith('QUERY_PF_NAT '):
|
|
||||||
try:
|
|
||||||
dst = pf_query_nat(*(line[13:].split(',')))
|
|
||||||
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
|
|
||||||
except IOError, e:
|
|
||||||
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
|
|
||||||
|
|
||||||
sys.stdout.flush()
|
|
||||||
elif line:
|
|
||||||
raise Fatal('expected EOF, got %r' % line)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
debug1('firewall manager: undoing changes.\n')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if port_v6:
|
|
||||||
do_it(port_v6, 0, socket.AF_INET6, [], udp)
|
|
||||||
if port_v4:
|
|
||||||
do_it(port_v4, 0, socket.AF_INET, [], udp)
|
|
||||||
restore_etc_hosts(port_v6 or port_v4)
|
|
12
src/sshuttle
12
src/sshuttle
@ -1,12 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
EXE=$0
|
|
||||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
|
||||||
[ -L "$EXE" ] || break
|
|
||||||
EXE=$(readlink "$EXE")
|
|
||||||
done
|
|
||||||
DIR=$(dirname "$EXE")
|
|
||||||
if python2 -V 2>/dev/null; then
|
|
||||||
exec python2 "$DIR/main.py" python2 "$@"
|
|
||||||
else
|
|
||||||
exec python "$DIR/main.py" python "$@"
|
|
||||||
fi
|
|
8
src/ui-macos/.gitignore
vendored
8
src/ui-macos/.gitignore
vendored
@ -1,8 +0,0 @@
|
|||||||
*.pyc
|
|
||||||
*~
|
|
||||||
/*.nib
|
|
||||||
/debug.app
|
|
||||||
/sources.list
|
|
||||||
/Sshuttle VPN.app
|
|
||||||
/*.tar.gz
|
|
||||||
/*.zip
|
|
@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>English</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>Sshuttle VPN</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>Sshuttle</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>app.icns</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>ca.apenwarr.Sshuttle</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>Sshuttle VPN</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>0.0.0</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>0.0.0</string>
|
|
||||||
<key>LSUIElement</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>LSHasLocalizedDisplayName</key>
|
|
||||||
<false/>
|
|
||||||
<key>NSAppleScriptEnabled</key>
|
|
||||||
<false/>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>GNU LGPL Version 2</string>
|
|
||||||
<key>NSMainNibFile</key>
|
|
||||||
<string>MainMenu</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
File diff suppressed because it is too large
Load Diff
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>startAtLogin</key>
|
|
||||||
<false/>
|
|
||||||
<key>autoReconnect</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1 +0,0 @@
|
|||||||
redo-ifchange debug.app dist
|
|
Binary file not shown.
@ -1,30 +0,0 @@
|
|||||||
import re
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
def askpass(prompt):
|
|
||||||
prompt = prompt.replace('"', "'")
|
|
||||||
|
|
||||||
if 'yes/no' in prompt:
|
|
||||||
return "yes"
|
|
||||||
|
|
||||||
script = """
|
|
||||||
tell application "Finder"
|
|
||||||
activate
|
|
||||||
display dialog "%s" \
|
|
||||||
with title "Sshuttle SSH Connection" \
|
|
||||||
default answer "" \
|
|
||||||
with icon caution \
|
|
||||||
with hidden answer
|
|
||||||
end tell
|
|
||||||
""" % prompt
|
|
||||||
|
|
||||||
p = subprocess.Popen(['osascript', '-e', script], stdout=subprocess.PIPE)
|
|
||||||
out = p.stdout.read()
|
|
||||||
rv = p.wait()
|
|
||||||
if rv:
|
|
||||||
return None
|
|
||||||
g = re.match("text returned:(.*), button returned:.*", out)
|
|
||||||
if not g:
|
|
||||||
return None
|
|
||||||
return g.group(1)
|
|
1
src/ui-macos/bits/.gitignore
vendored
1
src/ui-macos/bits/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/runpython
|
|
@ -1 +0,0 @@
|
|||||||
APPL????
|
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* This rather pointless program acts like the python interpreter, except
|
|
||||||
* it's intended to sit inside a MacOS .app package, so that its argv[0]
|
|
||||||
* will point inside the package.
|
|
||||||
*
|
|
||||||
* NSApplicationMain() looks for Info.plist using the path in argv[0], which
|
|
||||||
* goes wrong if your interpreter is /usr/bin/python.
|
|
||||||
*/
|
|
||||||
#include <Python/Python.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
|
||||||
{
|
|
||||||
char *path = strdup(argv[0]), *cptr;
|
|
||||||
char *args[] = {argv[0], "../Resources/main.py", NULL};
|
|
||||||
cptr = strrchr(path, '/');
|
|
||||||
if (cptr)
|
|
||||||
*cptr = 0;
|
|
||||||
chdir(path);
|
|
||||||
free(path);
|
|
||||||
return Py_Main(2, args);
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
exec >&2
|
|
||||||
redo-ifchange runpython.c
|
|
||||||
ARCHES=""
|
|
||||||
printf "Platforms: "
|
|
||||||
if [ -d /usr/libexec/gcc/darwin ]; then
|
|
||||||
for d in /usr/libexec/gcc/darwin/*; do
|
|
||||||
PLAT=$(basename "$d")
|
|
||||||
[ "$PLAT" != "ppc64" ] || continue # fails for some reason on my Mac
|
|
||||||
ARCHES="$ARCHES -arch $PLAT"
|
|
||||||
printf "$PLAT "
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
printf "\n"
|
|
||||||
PYTHON_LDFLAGS=$(python-config --ldflags)
|
|
||||||
PYTHON_INCLUDES=$(python-config --includes)
|
|
||||||
gcc $ARCHES \
|
|
||||||
-Wall -o $3 runpython.c \
|
|
||||||
$PYTHON_INCLUDES \
|
|
||||||
$PYTHON_LDFLAGS \
|
|
||||||
-framework Python
|
|
Binary file not shown.
Before Width: | Height: | Size: 821 B |
Binary file not shown.
Before Width: | Height: | Size: 789 B |
Binary file not shown.
Before Width: | Height: | Size: 810 B |
@ -1,4 +0,0 @@
|
|||||||
exec >&2
|
|
||||||
find . -name '*~' | xargs rm -f
|
|
||||||
rm -rf *.app *.zip *.tar.gz
|
|
||||||
rm -f bits/runpython *.nib sources.list
|
|
@ -1,15 +0,0 @@
|
|||||||
redo-ifchange bits/runpython MainMenu.nib
|
|
||||||
rm -rf debug.app
|
|
||||||
mkdir debug.app debug.app/Contents
|
|
||||||
cd debug.app/Contents
|
|
||||||
ln -s ../.. Resources
|
|
||||||
ln -s ../.. English.lproj
|
|
||||||
ln -s ../../Info.plist .
|
|
||||||
ln -s ../../app.icns .
|
|
||||||
|
|
||||||
mkdir MacOS
|
|
||||||
cd MacOS
|
|
||||||
ln -s ../../../bits/runpython Sshuttle
|
|
||||||
|
|
||||||
cd ../../..
|
|
||||||
redo-ifchange $(find debug.app -type f)
|
|
@ -1,28 +0,0 @@
|
|||||||
TOP=$PWD
|
|
||||||
redo-ifchange sources.list
|
|
||||||
redo-ifchange Info.plist bits/runpython \
|
|
||||||
$(while read name newname; do echo "$name"; done <sources.list)
|
|
||||||
|
|
||||||
rm -rf "$2.app"
|
|
||||||
mkdir "$2.app" "$2.app/Contents"
|
|
||||||
cd "$2.app/Contents"
|
|
||||||
|
|
||||||
cp "$TOP/Info.plist" .
|
|
||||||
|
|
||||||
mkdir MacOS
|
|
||||||
cp "$TOP/bits/runpython" MacOS/Sshuttle
|
|
||||||
|
|
||||||
mkdir Resources
|
|
||||||
|
|
||||||
cd "$TOP"
|
|
||||||
while read name newname; do
|
|
||||||
[ -z "$name" ] && continue
|
|
||||||
: "${newname:=$name}"
|
|
||||||
outname=$2.app/Contents/Resources/$newname
|
|
||||||
outdir=$(dirname "$outname")
|
|
||||||
[ -d "$outdir" ] || mkdir "$outdir"
|
|
||||||
cp "${name-$newname}" "$outname"
|
|
||||||
done <sources.list
|
|
||||||
|
|
||||||
cd "$2.app"
|
|
||||||
redo-ifchange $(find . -type f)
|
|
@ -1,5 +0,0 @@
|
|||||||
exec >&2
|
|
||||||
IFS="
|
|
||||||
"
|
|
||||||
redo-ifchange $2.app
|
|
||||||
tar -czf $3 $2.app/
|
|
@ -1,5 +0,0 @@
|
|||||||
exec >&2
|
|
||||||
IFS="
|
|
||||||
"
|
|
||||||
redo-ifchange $2.app
|
|
||||||
zip -q -r $3 $2.app/
|
|
@ -1,2 +0,0 @@
|
|||||||
redo-ifchange $2.xib
|
|
||||||
ibtool --compile $3 $2.xib
|
|
@ -1 +0,0 @@
|
|||||||
redo-ifchange "Sshuttle VPN.app.zip" "Sshuttle VPN.app.tar.gz"
|
|
@ -1,19 +0,0 @@
|
|||||||
# update a local branch with pregenerated output files, so people can download
|
|
||||||
# the completed tarballs from github. Since we don't have any real binaries,
|
|
||||||
# our final distribution package contains mostly blobs from the source code,
|
|
||||||
# so this doesn't cost us much extra space in the repo.
|
|
||||||
BRANCH=dist/macos
|
|
||||||
redo-ifchange 'Sshuttle VPN.app'
|
|
||||||
git update-ref refs/heads/$BRANCH origin/$BRANCH '' 2>/dev/null || true
|
|
||||||
|
|
||||||
export GIT_INDEX_FILE=$PWD/gitindex.tmp
|
|
||||||
rm -f "$GIT_INDEX_FILE"
|
|
||||||
git add -f 'Sshuttle VPN.app'
|
|
||||||
|
|
||||||
MSG="MacOS precompiled app package for $(git describe)"
|
|
||||||
TREE=$(git write-tree --prefix=ui-macos)
|
|
||||||
git show-ref refs/heads/$BRANCH >/dev/null && PARENT="-p refs/heads/$BRANCH"
|
|
||||||
COMMITID=$(echo "$MSG" | git commit-tree $TREE $PARENT)
|
|
||||||
|
|
||||||
git update-ref refs/heads/$BRANCH $COMMITID
|
|
||||||
rm -f "$GIT_INDEX_FILE"
|
|
@ -1,387 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
import pty
|
|
||||||
from AppKit import *
|
|
||||||
import my
|
|
||||||
import models
|
|
||||||
import askpass
|
|
||||||
|
|
||||||
|
|
||||||
def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug,
|
|
||||||
no_latency_control):
|
|
||||||
argv = [my.bundle_path('sshuttle/sshuttle', ''), '-r', host]
|
|
||||||
assert(argv[0])
|
|
||||||
if debug:
|
|
||||||
argv.append('-v')
|
|
||||||
if auto_nets:
|
|
||||||
argv.append('--auto-nets')
|
|
||||||
if auto_hosts:
|
|
||||||
argv.append('--auto-hosts')
|
|
||||||
if dns:
|
|
||||||
argv.append('--dns')
|
|
||||||
if no_latency_control:
|
|
||||||
argv.append('--no-latency-control')
|
|
||||||
argv += nets
|
|
||||||
return argv
|
|
||||||
|
|
||||||
|
|
||||||
class _Callback(NSObject):
|
|
||||||
|
|
||||||
def initWithFunc_(self, func):
|
|
||||||
self = super(_Callback, self).init()
|
|
||||||
self.func = func
|
|
||||||
return self
|
|
||||||
|
|
||||||
def func_(self, obj):
|
|
||||||
return self.func(obj)
|
|
||||||
|
|
||||||
|
|
||||||
class Callback:
|
|
||||||
|
|
||||||
def __init__(self, func):
|
|
||||||
self.obj = _Callback.alloc().initWithFunc_(func)
|
|
||||||
self.sel = self.obj.func_
|
|
||||||
|
|
||||||
|
|
||||||
class Runner:
|
|
||||||
|
|
||||||
def __init__(self, argv, logfunc, promptfunc, serverobj):
|
|
||||||
print 'in __init__'
|
|
||||||
self.id = argv
|
|
||||||
self.rv = None
|
|
||||||
self.pid = None
|
|
||||||
self.fd = None
|
|
||||||
self.logfunc = logfunc
|
|
||||||
self.promptfunc = promptfunc
|
|
||||||
self.serverobj = serverobj
|
|
||||||
self.buf = ''
|
|
||||||
self.logfunc('\nConnecting to %s.\n' % self.serverobj.host())
|
|
||||||
print 'will run: %r' % argv
|
|
||||||
self.serverobj.setConnected_(False)
|
|
||||||
pid, fd = pty.fork()
|
|
||||||
if pid == 0:
|
|
||||||
# child
|
|
||||||
try:
|
|
||||||
os.execvp(argv[0], argv)
|
|
||||||
except Exception, e:
|
|
||||||
sys.stderr.write('failed to start: %r\n' % e)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
os._exit(42)
|
|
||||||
# parent
|
|
||||||
self.pid = pid
|
|
||||||
self.file = NSFileHandle.alloc()\
|
|
||||||
.initWithFileDescriptor_closeOnDealloc_(fd, True)
|
|
||||||
self.cb = Callback(self.gotdata)
|
|
||||||
NSNotificationCenter.defaultCenter()\
|
|
||||||
.addObserver_selector_name_object_(
|
|
||||||
self.cb.obj, self.cb.sel,
|
|
||||||
NSFileHandleDataAvailableNotification, self.file)
|
|
||||||
self.file.waitForDataInBackgroundAndNotify()
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.wait()
|
|
||||||
|
|
||||||
def _try_wait(self, options):
|
|
||||||
if self.rv is None and self.pid > 0:
|
|
||||||
pid, code = os.waitpid(self.pid, options)
|
|
||||||
if pid == self.pid:
|
|
||||||
if os.WIFEXITED(code):
|
|
||||||
self.rv = os.WEXITSTATUS(code)
|
|
||||||
else:
|
|
||||||
self.rv = -os.WSTOPSIG(code)
|
|
||||||
self.serverobj.setConnected_(False)
|
|
||||||
self.serverobj.setError_('VPN process died')
|
|
||||||
self.logfunc('Disconnected.\n')
|
|
||||||
print 'wait_result: %r' % self.rv
|
|
||||||
return self.rv
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
rv = None
|
|
||||||
while rv is None:
|
|
||||||
self.gotdata(None)
|
|
||||||
rv = self._try_wait(os.WNOHANG)
|
|
||||||
|
|
||||||
def poll(self):
|
|
||||||
return self._try_wait(os.WNOHANG)
|
|
||||||
|
|
||||||
def kill(self):
|
|
||||||
assert(self.pid > 0)
|
|
||||||
print 'killing: pid=%r rv=%r' % (self.pid, self.rv)
|
|
||||||
if self.rv is None:
|
|
||||||
self.logfunc('Disconnecting from %s.\n' % self.serverobj.host())
|
|
||||||
os.kill(self.pid, 15)
|
|
||||||
self.wait()
|
|
||||||
|
|
||||||
def gotdata(self, notification):
|
|
||||||
print 'gotdata!'
|
|
||||||
d = str(self.file.availableData())
|
|
||||||
if d:
|
|
||||||
self.logfunc(d)
|
|
||||||
self.buf = self.buf + d
|
|
||||||
if 'Connected.\r\n' in self.buf:
|
|
||||||
self.serverobj.setConnected_(True)
|
|
||||||
self.buf = self.buf[-4096:]
|
|
||||||
if self.buf.strip().endswith(':'):
|
|
||||||
lastline = self.buf.rstrip().split('\n')[-1]
|
|
||||||
resp = self.promptfunc(lastline)
|
|
||||||
add = ' (response)\n'
|
|
||||||
self.buf += add
|
|
||||||
self.logfunc(add)
|
|
||||||
self.file.writeData_(my.Data(resp + '\n'))
|
|
||||||
self.file.waitForDataInBackgroundAndNotify()
|
|
||||||
self.poll()
|
|
||||||
# print 'gotdata done!'
|
|
||||||
|
|
||||||
|
|
||||||
class SshuttleApp(NSObject):
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
d = my.PList('UserDefaults')
|
|
||||||
my.Defaults().registerDefaults_(d)
|
|
||||||
|
|
||||||
|
|
||||||
class SshuttleController(NSObject):
|
|
||||||
# Interface builder outlets
|
|
||||||
startAtLoginField = objc.IBOutlet()
|
|
||||||
autoReconnectField = objc.IBOutlet()
|
|
||||||
debugField = objc.IBOutlet()
|
|
||||||
routingField = objc.IBOutlet()
|
|
||||||
prefsWindow = objc.IBOutlet()
|
|
||||||
serversController = objc.IBOutlet()
|
|
||||||
logField = objc.IBOutlet()
|
|
||||||
latencyControlField = objc.IBOutlet()
|
|
||||||
|
|
||||||
servers = []
|
|
||||||
conns = {}
|
|
||||||
|
|
||||||
def _connect(self, server):
|
|
||||||
host = server.host()
|
|
||||||
print 'connecting %r' % host
|
|
||||||
self.fill_menu()
|
|
||||||
|
|
||||||
def logfunc(msg):
|
|
||||||
print 'log! (%d bytes)' % len(msg)
|
|
||||||
self.logField.textStorage()\
|
|
||||||
.appendAttributedString_(NSAttributedString.alloc()
|
|
||||||
.initWithString_(msg))
|
|
||||||
self.logField.didChangeText()
|
|
||||||
|
|
||||||
def promptfunc(prompt):
|
|
||||||
print 'prompt! %r' % prompt
|
|
||||||
return askpass.askpass(prompt)
|
|
||||||
nets_mode = server.autoNets()
|
|
||||||
if nets_mode == models.NET_MANUAL:
|
|
||||||
manual_nets = ["%s/%d" % (i.subnet(), i.width())
|
|
||||||
for i in server.nets()]
|
|
||||||
elif nets_mode == models.NET_ALL:
|
|
||||||
manual_nets = ['0/0']
|
|
||||||
else:
|
|
||||||
manual_nets = []
|
|
||||||
noLatencyControl = (server.latencyControl() != models.LAT_INTERACTIVE)
|
|
||||||
conn = Runner(sshuttle_args(host,
|
|
||||||
auto_nets=nets_mode == models.NET_AUTO,
|
|
||||||
auto_hosts=server.autoHosts(),
|
|
||||||
dns=server.useDns(),
|
|
||||||
nets=manual_nets,
|
|
||||||
debug=self.debugField.state(),
|
|
||||||
no_latency_control=noLatencyControl),
|
|
||||||
logfunc=logfunc, promptfunc=promptfunc,
|
|
||||||
serverobj=server)
|
|
||||||
self.conns[host] = conn
|
|
||||||
|
|
||||||
def _disconnect(self, server):
|
|
||||||
host = server.host()
|
|
||||||
print 'disconnecting %r' % host
|
|
||||||
conn = self.conns.get(host)
|
|
||||||
if conn:
|
|
||||||
conn.kill()
|
|
||||||
self.fill_menu()
|
|
||||||
self.logField.textStorage().setAttributedString_(
|
|
||||||
NSAttributedString.alloc().initWithString_(''))
|
|
||||||
|
|
||||||
@objc.IBAction
|
|
||||||
def cmd_connect(self, sender):
|
|
||||||
server = sender.representedObject()
|
|
||||||
server.setWantConnect_(True)
|
|
||||||
|
|
||||||
@objc.IBAction
|
|
||||||
def cmd_disconnect(self, sender):
|
|
||||||
server = sender.representedObject()
|
|
||||||
server.setWantConnect_(False)
|
|
||||||
|
|
||||||
@objc.IBAction
|
|
||||||
def cmd_show(self, sender):
|
|
||||||
self.prefsWindow.makeKeyAndOrderFront_(self)
|
|
||||||
NSApp.activateIgnoringOtherApps_(True)
|
|
||||||
|
|
||||||
@objc.IBAction
|
|
||||||
def cmd_quit(self, sender):
|
|
||||||
NSApp.performSelector_withObject_afterDelay_(NSApp.terminate_,
|
|
||||||
None, 0.0)
|
|
||||||
|
|
||||||
def fill_menu(self):
|
|
||||||
menu = self.menu
|
|
||||||
menu.removeAllItems()
|
|
||||||
|
|
||||||
def additem(name, func, obj):
|
|
||||||
it = menu.addItemWithTitle_action_keyEquivalent_(name, None, "")
|
|
||||||
it.setRepresentedObject_(obj)
|
|
||||||
it.setTarget_(self)
|
|
||||||
it.setAction_(func)
|
|
||||||
|
|
||||||
def addnote(name):
|
|
||||||
additem(name, None, None)
|
|
||||||
|
|
||||||
any_inprogress = None
|
|
||||||
any_conn = None
|
|
||||||
any_err = None
|
|
||||||
if len(self.servers):
|
|
||||||
for i in self.servers:
|
|
||||||
host = i.host()
|
|
||||||
title = i.title()
|
|
||||||
want = i.wantConnect()
|
|
||||||
connected = i.connected()
|
|
||||||
numnets = len(list(i.nets()))
|
|
||||||
if not host:
|
|
||||||
additem('Connect Untitled', None, i)
|
|
||||||
elif i.autoNets() == models.NET_MANUAL and not numnets:
|
|
||||||
additem('Connect %s (no routes)' % host, None, i)
|
|
||||||
elif want:
|
|
||||||
any_conn = i
|
|
||||||
additem('Disconnect %s' % title, self.cmd_disconnect, i)
|
|
||||||
else:
|
|
||||||
additem('Connect %s' % title, self.cmd_connect, i)
|
|
||||||
if not want:
|
|
||||||
msg = 'Off'
|
|
||||||
elif i.error():
|
|
||||||
msg = 'ERROR - try reconnecting'
|
|
||||||
any_err = i
|
|
||||||
elif connected:
|
|
||||||
msg = 'Connected'
|
|
||||||
else:
|
|
||||||
msg = 'Connecting...'
|
|
||||||
any_inprogress = i
|
|
||||||
addnote(' State: %s' % msg)
|
|
||||||
else:
|
|
||||||
addnote('No servers defined yet')
|
|
||||||
|
|
||||||
menu.addItem_(NSMenuItem.separatorItem())
|
|
||||||
additem('Preferences...', self.cmd_show, None)
|
|
||||||
additem('Quit Sshuttle VPN', self.cmd_quit, None)
|
|
||||||
|
|
||||||
if any_err:
|
|
||||||
self.statusitem.setImage_(self.img_err)
|
|
||||||
self.statusitem.setTitle_('Error!')
|
|
||||||
elif any_conn:
|
|
||||||
self.statusitem.setImage_(self.img_running)
|
|
||||||
if any_inprogress:
|
|
||||||
self.statusitem.setTitle_('Connecting...')
|
|
||||||
else:
|
|
||||||
self.statusitem.setTitle_('')
|
|
||||||
else:
|
|
||||||
self.statusitem.setImage_(self.img_idle)
|
|
||||||
self.statusitem.setTitle_('')
|
|
||||||
|
|
||||||
def load_servers(self):
|
|
||||||
l = my.Defaults().arrayForKey_('servers') or []
|
|
||||||
sl = []
|
|
||||||
for s in l:
|
|
||||||
host = s.get('host', None)
|
|
||||||
if not host:
|
|
||||||
continue
|
|
||||||
|
|
||||||
nets = s.get('nets', [])
|
|
||||||
nl = []
|
|
||||||
for n in nets:
|
|
||||||
subnet = n[0]
|
|
||||||
width = n[1]
|
|
||||||
net = models.SshuttleNet.alloc().init()
|
|
||||||
net.setSubnet_(subnet)
|
|
||||||
net.setWidth_(width)
|
|
||||||
nl.append(net)
|
|
||||||
|
|
||||||
autoNets = s.get('autoNets', models.NET_AUTO)
|
|
||||||
autoHosts = s.get('autoHosts', True)
|
|
||||||
useDns = s.get('useDns', autoNets == models.NET_ALL)
|
|
||||||
latencyControl = s.get('latencyControl', models.LAT_INTERACTIVE)
|
|
||||||
srv = models.SshuttleServer.alloc().init()
|
|
||||||
srv.setHost_(host)
|
|
||||||
srv.setAutoNets_(autoNets)
|
|
||||||
srv.setAutoHosts_(autoHosts)
|
|
||||||
srv.setNets_(nl)
|
|
||||||
srv.setUseDns_(useDns)
|
|
||||||
srv.setLatencyControl_(latencyControl)
|
|
||||||
sl.append(srv)
|
|
||||||
self.serversController.addObjects_(sl)
|
|
||||||
self.serversController.setSelectionIndex_(0)
|
|
||||||
|
|
||||||
def save_servers(self):
|
|
||||||
l = []
|
|
||||||
for s in self.servers:
|
|
||||||
host = s.host()
|
|
||||||
if not host:
|
|
||||||
continue
|
|
||||||
nets = []
|
|
||||||
for n in s.nets():
|
|
||||||
subnet = n.subnet()
|
|
||||||
if not subnet:
|
|
||||||
continue
|
|
||||||
nets.append((subnet, n.width()))
|
|
||||||
d = dict(host=s.host(),
|
|
||||||
nets=nets,
|
|
||||||
autoNets=s.autoNets(),
|
|
||||||
autoHosts=s.autoHosts(),
|
|
||||||
useDns=s.useDns(),
|
|
||||||
latencyControl=s.latencyControl())
|
|
||||||
l.append(d)
|
|
||||||
my.Defaults().setObject_forKey_(l, 'servers')
|
|
||||||
self.fill_menu()
|
|
||||||
|
|
||||||
def awakeFromNib(self):
|
|
||||||
self.routingField.removeAllItems()
|
|
||||||
tf = self.routingField.addItemWithTitle_
|
|
||||||
tf('Send all traffic through this server')
|
|
||||||
tf('Determine automatically')
|
|
||||||
tf('Custom...')
|
|
||||||
|
|
||||||
self.latencyControlField.removeAllItems()
|
|
||||||
tf = self.latencyControlField.addItemWithTitle_
|
|
||||||
tf('Fast transfer')
|
|
||||||
tf('Low latency')
|
|
||||||
|
|
||||||
# Hmm, even when I mark this as !enabled in the .nib, it still comes
|
|
||||||
# through as enabled. So let's just disable it here (since we don't
|
|
||||||
# support this feature yet).
|
|
||||||
self.startAtLoginField.setEnabled_(False)
|
|
||||||
self.startAtLoginField.setState_(False)
|
|
||||||
self.autoReconnectField.setEnabled_(False)
|
|
||||||
self.autoReconnectField.setState_(False)
|
|
||||||
|
|
||||||
self.load_servers()
|
|
||||||
|
|
||||||
# Initialize our menu item
|
|
||||||
self.menu = NSMenu.alloc().initWithTitle_('Sshuttle')
|
|
||||||
bar = NSStatusBar.systemStatusBar()
|
|
||||||
statusitem = bar.statusItemWithLength_(NSVariableStatusItemLength)
|
|
||||||
self.statusitem = statusitem
|
|
||||||
self.img_idle = my.Image('chicken-tiny-bw', 'png')
|
|
||||||
self.img_running = my.Image('chicken-tiny', 'png')
|
|
||||||
self.img_err = my.Image('chicken-tiny-err', 'png')
|
|
||||||
statusitem.setImage_(self.img_idle)
|
|
||||||
statusitem.setHighlightMode_(True)
|
|
||||||
statusitem.setMenu_(self.menu)
|
|
||||||
self.fill_menu()
|
|
||||||
|
|
||||||
models.configchange_callback = my.DelayedCallback(self.save_servers)
|
|
||||||
|
|
||||||
def sc(server):
|
|
||||||
if server.wantConnect():
|
|
||||||
self._connect(server)
|
|
||||||
else:
|
|
||||||
self._disconnect(server)
|
|
||||||
models.setconnect_callback = sc
|
|
||||||
|
|
||||||
|
|
||||||
# Note: NSApplicationMain calls sys.exit(), so this never returns.
|
|
||||||
NSApplicationMain(sys.argv)
|
|
@ -1,189 +0,0 @@
|
|||||||
from AppKit import *
|
|
||||||
import my
|
|
||||||
|
|
||||||
|
|
||||||
configchange_callback = setconnect_callback = None
|
|
||||||
objc_validator = objc.signature('@@:N^@o^@')
|
|
||||||
|
|
||||||
|
|
||||||
def config_changed():
|
|
||||||
if configchange_callback:
|
|
||||||
configchange_callback()
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_ip(v):
|
|
||||||
parts = v.split('.')[:4]
|
|
||||||
if len(parts) < 4:
|
|
||||||
parts += ['0'] * (4 - len(parts))
|
|
||||||
for i in range(4):
|
|
||||||
n = my.atoi(parts[i])
|
|
||||||
if n < 0:
|
|
||||||
n = 0
|
|
||||||
elif n > 255:
|
|
||||||
n = 255
|
|
||||||
parts[i] = str(n)
|
|
||||||
return '.'.join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_width(v):
|
|
||||||
n = my.atoi(v)
|
|
||||||
if n < 0:
|
|
||||||
n = 0
|
|
||||||
elif n > 32:
|
|
||||||
n = 32
|
|
||||||
return n
|
|
||||||
|
|
||||||
|
|
||||||
class SshuttleNet(NSObject):
|
|
||||||
|
|
||||||
def subnet(self):
|
|
||||||
return getattr(self, '_k_subnet', None)
|
|
||||||
|
|
||||||
def setSubnet_(self, v):
|
|
||||||
self._k_subnet = v
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
@objc_validator
|
|
||||||
def validateSubnet_error_(self, value, error):
|
|
||||||
# print 'validateSubnet!'
|
|
||||||
return True, _validate_ip(value), error
|
|
||||||
|
|
||||||
def width(self):
|
|
||||||
return getattr(self, '_k_width', 24)
|
|
||||||
|
|
||||||
def setWidth_(self, v):
|
|
||||||
self._k_width = v
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
@objc_validator
|
|
||||||
def validateWidth_error_(self, value, error):
|
|
||||||
# print 'validateWidth!'
|
|
||||||
return True, _validate_width(value), error
|
|
||||||
|
|
||||||
NET_ALL = 0
|
|
||||||
NET_AUTO = 1
|
|
||||||
NET_MANUAL = 2
|
|
||||||
|
|
||||||
LAT_BANDWIDTH = 0
|
|
||||||
LAT_INTERACTIVE = 1
|
|
||||||
|
|
||||||
|
|
||||||
class SshuttleServer(NSObject):
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
self = super(SshuttleServer, self).init()
|
|
||||||
config_changed()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def wantConnect(self):
|
|
||||||
return getattr(self, '_k_wantconnect', False)
|
|
||||||
|
|
||||||
def setWantConnect_(self, v):
|
|
||||||
self._k_wantconnect = v
|
|
||||||
self.setError_(None)
|
|
||||||
config_changed()
|
|
||||||
if setconnect_callback:
|
|
||||||
setconnect_callback(self)
|
|
||||||
|
|
||||||
def connected(self):
|
|
||||||
return getattr(self, '_k_connected', False)
|
|
||||||
|
|
||||||
def setConnected_(self, v):
|
|
||||||
print 'setConnected of %r to %r' % (self, v)
|
|
||||||
self._k_connected = v
|
|
||||||
if v:
|
|
||||||
self.setError_(None) # connected ok, so no error
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
def error(self):
|
|
||||||
return getattr(self, '_k_error', None)
|
|
||||||
|
|
||||||
def setError_(self, v):
|
|
||||||
self._k_error = v
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
def isValid(self):
|
|
||||||
if not self.host():
|
|
||||||
return False
|
|
||||||
if self.autoNets() == NET_MANUAL and not len(list(self.nets())):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def title(self):
|
|
||||||
host = self.host()
|
|
||||||
if not host:
|
|
||||||
return host
|
|
||||||
an = self.autoNets()
|
|
||||||
suffix = ""
|
|
||||||
if an == NET_ALL:
|
|
||||||
suffix = " (all traffic)"
|
|
||||||
elif an == NET_MANUAL:
|
|
||||||
n = self.nets()
|
|
||||||
suffix = ' (%d subnet%s)' % (len(n), len(n) != 1 and 's' or '')
|
|
||||||
return self.host() + suffix
|
|
||||||
|
|
||||||
def setTitle_(self, v):
|
|
||||||
# title is always auto-generated
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
def host(self):
|
|
||||||
return getattr(self, '_k_host', None)
|
|
||||||
|
|
||||||
def setHost_(self, v):
|
|
||||||
self._k_host = v
|
|
||||||
self.setTitle_(None)
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
@objc_validator
|
|
||||||
def validateHost_error_(self, value, error):
|
|
||||||
# print 'validatehost! %r %r %r' % (self, value, error)
|
|
||||||
while value.startswith('-'):
|
|
||||||
value = value[1:]
|
|
||||||
return True, value, error
|
|
||||||
|
|
||||||
def nets(self):
|
|
||||||
return getattr(self, '_k_nets', [])
|
|
||||||
|
|
||||||
def setNets_(self, v):
|
|
||||||
self._k_nets = v
|
|
||||||
self.setTitle_(None)
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
def netsHidden(self):
|
|
||||||
# print 'checking netsHidden'
|
|
||||||
return self.autoNets() != NET_MANUAL
|
|
||||||
|
|
||||||
def setNetsHidden_(self, v):
|
|
||||||
config_changed()
|
|
||||||
# print 'setting netsHidden to %r' % v
|
|
||||||
|
|
||||||
def autoNets(self):
|
|
||||||
return getattr(self, '_k_autoNets', NET_AUTO)
|
|
||||||
|
|
||||||
def setAutoNets_(self, v):
|
|
||||||
self._k_autoNets = v
|
|
||||||
self.setNetsHidden_(-1)
|
|
||||||
self.setUseDns_(v == NET_ALL)
|
|
||||||
self.setTitle_(None)
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
def autoHosts(self):
|
|
||||||
return getattr(self, '_k_autoHosts', True)
|
|
||||||
|
|
||||||
def setAutoHosts_(self, v):
|
|
||||||
self._k_autoHosts = v
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
def useDns(self):
|
|
||||||
return getattr(self, '_k_useDns', False)
|
|
||||||
|
|
||||||
def setUseDns_(self, v):
|
|
||||||
self._k_useDns = v
|
|
||||||
config_changed()
|
|
||||||
|
|
||||||
def latencyControl(self):
|
|
||||||
return getattr(self, '_k_latencyControl', LAT_INTERACTIVE)
|
|
||||||
|
|
||||||
def setLatencyControl_(self, v):
|
|
||||||
self._k_latencyControl = v
|
|
||||||
config_changed()
|
|
@ -1,64 +0,0 @@
|
|||||||
import os
|
|
||||||
from AppKit import *
|
|
||||||
import PyObjCTools.AppHelper
|
|
||||||
|
|
||||||
|
|
||||||
def bundle_path(name, typ):
|
|
||||||
if typ:
|
|
||||||
return NSBundle.mainBundle().pathForResource_ofType_(name, typ)
|
|
||||||
else:
|
|
||||||
return os.path.join(NSBundle.mainBundle().resourcePath(), name)
|
|
||||||
|
|
||||||
|
|
||||||
# Load an NSData using a python string
|
|
||||||
def Data(s):
|
|
||||||
return NSData.alloc().initWithBytes_length_(s, len(s))
|
|
||||||
|
|
||||||
|
|
||||||
# Load a property list from a file in the application bundle.
|
|
||||||
def PList(name):
|
|
||||||
path = bundle_path(name, 'plist')
|
|
||||||
return NSDictionary.dictionaryWithContentsOfFile_(path)
|
|
||||||
|
|
||||||
|
|
||||||
# Load an NSImage from a file in the application bundle.
|
|
||||||
def Image(name, ext):
|
|
||||||
bytes = open(bundle_path(name, ext)).read()
|
|
||||||
img = NSImage.alloc().initWithData_(Data(bytes))
|
|
||||||
return img
|
|
||||||
|
|
||||||
|
|
||||||
# Return the NSUserDefaults shared object.
|
|
||||||
def Defaults():
|
|
||||||
return NSUserDefaults.standardUserDefaults()
|
|
||||||
|
|
||||||
|
|
||||||
# Usage:
|
|
||||||
# f = DelayedCallback(func, args...)
|
|
||||||
# later:
|
|
||||||
# f()
|
|
||||||
#
|
|
||||||
# When you call f(), it will schedule a call to func() next time the
|
|
||||||
# ObjC event loop iterates. Multiple calls to f() in a single iteration
|
|
||||||
# will only result in one call to func().
|
|
||||||
#
|
|
||||||
def DelayedCallback(func, *args, **kwargs):
|
|
||||||
flag = [0]
|
|
||||||
|
|
||||||
def _go():
|
|
||||||
if flag[0]:
|
|
||||||
print 'running %r (flag=%r)' % (func, flag)
|
|
||||||
flag[0] = 0
|
|
||||||
func(*args, **kwargs)
|
|
||||||
|
|
||||||
def call():
|
|
||||||
flag[0] += 1
|
|
||||||
PyObjCTools.AppHelper.callAfter(_go)
|
|
||||||
return call
|
|
||||||
|
|
||||||
|
|
||||||
def atoi(s):
|
|
||||||
try:
|
|
||||||
return int(s)
|
|
||||||
except ValueError:
|
|
||||||
return 0
|
|
@ -1,4 +0,0 @@
|
|||||||
redo-ifchange debug.app
|
|
||||||
exec >&2
|
|
||||||
./debug.app/Contents/MacOS/Sshuttle
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
redo-always
|
|
||||||
exec >$3
|
|
||||||
cat <<-EOF
|
|
||||||
app.icns
|
|
||||||
MainMenu.nib English.lproj/MainMenu.nib
|
|
||||||
UserDefaults.plist
|
|
||||||
chicken-tiny.png
|
|
||||||
chicken-tiny-bw.png
|
|
||||||
chicken-tiny-err.png
|
|
||||||
EOF
|
|
||||||
for d in *.py sshuttle/*.py sshuttle/sshuttle sshuttle/compat/*.py; do
|
|
||||||
echo $d
|
|
||||||
done
|
|
||||||
redo-stamp <$3
|
|
@ -1 +0,0 @@
|
|||||||
..
|
|
@ -1,13 +1,12 @@
|
|||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import helpers
|
import sshuttle.helpers as helpers
|
||||||
import options
|
import sshuttle.options as options
|
||||||
import client
|
import sshuttle.client as client
|
||||||
import server
|
import sshuttle.firewall as firewall
|
||||||
import firewall
|
import sshuttle.hostwatch as hostwatch
|
||||||
import hostwatch
|
from sshuttle.helpers import family_ip_tuple, log, Fatal
|
||||||
from helpers import log, Fatal
|
|
||||||
|
|
||||||
|
|
||||||
# 1.2.3.4/5 or just 1.2.3.4
|
# 1.2.3.4/5 or just 1.2.3.4
|
||||||
@ -106,9 +105,12 @@ def parse_ipport6(s):
|
|||||||
return (ip, port)
|
return (ip, port)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_list(list):
|
||||||
|
return re.split(r'[\s,]+', list.strip()) if list else []
|
||||||
|
|
||||||
|
|
||||||
optspec = """
|
optspec = """
|
||||||
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
|
||||||
sshuttle --server
|
|
||||||
sshuttle --firewall <port> <subnets...>
|
sshuttle --firewall <port> <subnets...>
|
||||||
sshuttle --hostwatch
|
sshuttle --hostwatch
|
||||||
--
|
--
|
||||||
@ -116,6 +118,7 @@ l,listen= transproxy to this ip address and port number
|
|||||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||||
N,auto-nets automatically determine subnets to route
|
N,auto-nets automatically determine subnets to route
|
||||||
dns capture local DNS requests and forward to the remote DNS server
|
dns capture local DNS requests and forward to the remote DNS server
|
||||||
|
ns-hosts= capture and forward remote DNS requests to the following servers
|
||||||
method= auto, nat, tproxy, pf or ipfw
|
method= auto, nat, tproxy, pf or ipfw
|
||||||
python= path to python interpreter on the remote server
|
python= path to python interpreter on the remote server
|
||||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
||||||
@ -135,27 +138,21 @@ firewall (internal use only)
|
|||||||
hostwatch (internal use only)
|
hostwatch (internal use only)
|
||||||
"""
|
"""
|
||||||
o = options.Options(optspec)
|
o = options.Options(optspec)
|
||||||
(opt, flags, extra) = o.parse(sys.argv[2:])
|
(opt, flags, extra) = o.parse(sys.argv[1:])
|
||||||
|
|
||||||
if opt.daemon:
|
if opt.daemon:
|
||||||
opt.syslog = 1
|
opt.syslog = 1
|
||||||
if opt.wrap:
|
if opt.wrap:
|
||||||
import ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
ssnet.MAX_CHANNEL = int(opt.wrap)
|
ssnet.MAX_CHANNEL = int(opt.wrap)
|
||||||
helpers.verbose = opt.verbose
|
helpers.verbose = opt.verbose
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if opt.server:
|
if opt.firewall:
|
||||||
if len(extra) != 0:
|
if len(extra) != 0:
|
||||||
o.fatal('no arguments expected')
|
o.fatal('exactly zero arguments expected')
|
||||||
server.latency_control = opt.latency_control
|
result = firewall.main(opt.method, opt.syslog)
|
||||||
sys.exit(server.main())
|
sys.exit(result)
|
||||||
elif opt.firewall:
|
|
||||||
if len(extra) != 6:
|
|
||||||
o.fatal('exactly six arguments expected')
|
|
||||||
sys.exit(firewall.main(int(extra[0]), int(extra[1]),
|
|
||||||
int(extra[2]), int(extra[3]),
|
|
||||||
extra[4], int(extra[5]), opt.syslog))
|
|
||||||
elif opt.hostwatch:
|
elif opt.hostwatch:
|
||||||
sys.exit(hostwatch.hw_main(extra))
|
sys.exit(hostwatch.hw_main(extra))
|
||||||
else:
|
else:
|
||||||
@ -171,6 +168,7 @@ try:
|
|||||||
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 parse_list(opt.ns_hosts)]
|
||||||
if opt.seed_hosts and not opt.auto_hosts:
|
if opt.seed_hosts and not opt.auto_hosts:
|
||||||
o.fatal('--seed-hosts only works if you also use -H')
|
o.fatal('--seed-hosts only works if you also use -H')
|
||||||
if opt.seed_hosts:
|
if opt.seed_hosts:
|
||||||
@ -182,23 +180,20 @@ try:
|
|||||||
if opt.subnets:
|
if opt.subnets:
|
||||||
includes = parse_subnet_file(opt.subnets)
|
includes = parse_subnet_file(opt.subnets)
|
||||||
if not opt.method:
|
if not opt.method:
|
||||||
method = "auto"
|
method_name = "auto"
|
||||||
elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]:
|
elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]:
|
||||||
method = opt.method
|
method_name = opt.method
|
||||||
else:
|
else:
|
||||||
o.fatal("method %s not supported" % opt.method)
|
o.fatal("method_name %s not supported" % opt.method)
|
||||||
if not opt.listen:
|
if not opt.listen:
|
||||||
if opt.method == "tproxy":
|
ipport_v6 = "auto" # parse_ipport6('[::1]:0')
|
||||||
ipport_v6 = parse_ipport6('[::1]:0')
|
ipport_v4 = "auto" # parse_ipport4('127.0.0.1:0')
|
||||||
else:
|
|
||||||
ipport_v6 = None
|
|
||||||
ipport_v4 = parse_ipport4('127.0.0.1:0')
|
|
||||||
else:
|
else:
|
||||||
ipport_v6 = None
|
ipport_v6 = None
|
||||||
ipport_v4 = None
|
ipport_v4 = None
|
||||||
list = opt.listen.split(",")
|
list = opt.listen.split(",")
|
||||||
for ip in list:
|
for ip in list:
|
||||||
if '[' in ip and ']' in ip and opt.method == "tproxy":
|
if '[' in ip and ']' in ip:
|
||||||
ipport_v6 = parse_ipport6(ip)
|
ipport_v6 = parse_ipport6(ip)
|
||||||
else:
|
else:
|
||||||
ipport_v4 = parse_ipport4(ip)
|
ipport_v4 = parse_ipport4(ip)
|
||||||
@ -208,7 +203,8 @@ try:
|
|||||||
opt.python,
|
opt.python,
|
||||||
opt.latency_control,
|
opt.latency_control,
|
||||||
opt.dns,
|
opt.dns,
|
||||||
method,
|
nslist,
|
||||||
|
method_name,
|
||||||
sh,
|
sh,
|
||||||
opt.auto_nets,
|
opt.auto_nets,
|
||||||
parse_subnets(includes),
|
parse_subnets(includes),
|
||||||
@ -221,7 +217,7 @@ try:
|
|||||||
log('Abnormal exit code detected, failing...' % return_code)
|
log('Abnormal exit code detected, failing...' % return_code)
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
except Fatal, e:
|
except Fatal as e:
|
||||||
log('fatal: %s\n' % e)
|
log('fatal: %s\n' % e)
|
||||||
sys.exit(99)
|
sys.exit(99)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
34
sshuttle/assembler.py
Normal file
34
sshuttle/assembler.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import sys
|
||||||
|
import zlib
|
||||||
|
import imp
|
||||||
|
|
||||||
|
z = zlib.decompressobj()
|
||||||
|
while 1:
|
||||||
|
name = sys.stdin.readline().strip()
|
||||||
|
if name:
|
||||||
|
nbytes = int(sys.stdin.readline())
|
||||||
|
if verbosity >= 2:
|
||||||
|
sys.stderr.write('server: assembling %r (%d bytes)\n'
|
||||||
|
% (name, nbytes))
|
||||||
|
content = z.decompress(sys.stdin.read(nbytes))
|
||||||
|
|
||||||
|
module = imp.new_module(name)
|
||||||
|
parent, _, parent_name = name.rpartition(".")
|
||||||
|
if parent != "":
|
||||||
|
setattr(sys.modules[parent], parent_name, module)
|
||||||
|
|
||||||
|
code = compile(content, name, "exec")
|
||||||
|
exec(code, module.__dict__)
|
||||||
|
sys.modules[name] = module
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
sys.stderr.flush()
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
import sshuttle.helpers
|
||||||
|
sshuttle.helpers.verbose = verbosity
|
||||||
|
|
||||||
|
import sshuttle.cmdline_options as options
|
||||||
|
from sshuttle.server import main
|
||||||
|
main(options.latency_control)
|
@ -1,34 +1,19 @@
|
|||||||
import struct
|
import socket
|
||||||
import errno
|
import errno
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
import compat.ssubprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
import helpers
|
import sshuttle.helpers as helpers
|
||||||
import os
|
import os
|
||||||
import ssnet
|
import sshuttle.ssnet as ssnet
|
||||||
import ssh
|
import sshuttle.ssh as ssh
|
||||||
import ssyslog
|
import sshuttle.ssyslog as ssyslog
|
||||||
import sys
|
import sys
|
||||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
from helpers import log, debug1, debug2, debug3, Fatal, islocal
|
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
||||||
|
resolvconf_nameservers
|
||||||
recvmsg = None
|
from sshuttle.methods import get_method
|
||||||
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
|
|
||||||
|
|
||||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||||
|
|
||||||
@ -39,83 +24,6 @@ def got_signal(signum, frame):
|
|||||||
|
|
||||||
|
|
||||||
_pidname = None
|
_pidname = None
|
||||||
IP_TRANSPARENT = 19
|
|
||||||
IP_ORIGDSTADDR = 20
|
|
||||||
IP_RECVORIGDSTADDR = IP_ORIGDSTADDR
|
|
||||||
SOL_IPV6 = 41
|
|
||||||
IPV6_ORIGDSTADDR = 74
|
|
||||||
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
|
||||||
|
|
||||||
|
|
||||||
if recvmsg == "python":
|
|
||||||
def recv_udp(listener, bufsize):
|
|
||||||
debug3('Accept UDP python using recvmsg.\n')
|
|
||||||
data, ancdata, msg_flags, srcip = listener.recvmsg(
|
|
||||||
4096, socket.CMSG_SPACE(24))
|
|
||||||
dstip = None
|
|
||||||
family = None
|
|
||||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
|
||||||
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
|
|
||||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
|
||||||
port = socket.htons(port)
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
start = 4
|
|
||||||
length = 4
|
|
||||||
else:
|
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
|
||||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
|
||||||
dstip = (ip, port)
|
|
||||||
break
|
|
||||||
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
|
|
||||||
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
|
||||||
port = socket.htons(port)
|
|
||||||
if family == socket.AF_INET6:
|
|
||||||
start = 8
|
|
||||||
length = 16
|
|
||||||
else:
|
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
|
||||||
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
|
||||||
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, flags = listener.recvmsg(
|
|
||||||
(bufsize,), socket.CMSG_SPACE(24))
|
|
||||||
dstip = None
|
|
||||||
family = None
|
|
||||||
for a in adata:
|
|
||||||
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
|
|
||||||
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
|
||||||
port = socket.htons(port)
|
|
||||||
if family == socket.AF_INET:
|
|
||||||
start = 4
|
|
||||||
length = 4
|
|
||||||
else:
|
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
|
||||||
ip = socket.inet_ntop(
|
|
||||||
family, a.cmsg_data[start:start + length])
|
|
||||||
dstip = (ip, port)
|
|
||||||
break
|
|
||||||
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
|
|
||||||
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
|
||||||
port = socket.htons(port)
|
|
||||||
if family == socket.AF_INET6:
|
|
||||||
start = 8
|
|
||||||
length = 16
|
|
||||||
else:
|
|
||||||
raise Fatal("Unsupported socket type '%s'" % family)
|
|
||||||
ip = socket.inet_ntop(
|
|
||||||
family, a.cmsg_data[start:start + length])
|
|
||||||
dstip = (ip, port)
|
|
||||||
break
|
|
||||||
return (srcip, dstip, data[0])
|
|
||||||
else:
|
|
||||||
def recv_udp(listener, bufsize):
|
|
||||||
debug3('Accept UDP using recvfrom.\n')
|
|
||||||
data, srcip = listener.recvfrom(bufsize)
|
|
||||||
return (srcip, None, data)
|
|
||||||
|
|
||||||
|
|
||||||
def check_daemon(pidfile):
|
def check_daemon(pidfile):
|
||||||
@ -123,7 +31,7 @@ def check_daemon(pidfile):
|
|||||||
_pidname = os.path.abspath(pidfile)
|
_pidname = os.path.abspath(pidfile)
|
||||||
try:
|
try:
|
||||||
oldpid = open(_pidname).read(1024)
|
oldpid = open(_pidname).read(1024)
|
||||||
except IOError, e:
|
except IOError as e:
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
return # no pidfile, ok
|
return # no pidfile, ok
|
||||||
else:
|
else:
|
||||||
@ -137,7 +45,7 @@ def check_daemon(pidfile):
|
|||||||
return # invalid pidfile, ok
|
return # invalid pidfile, ok
|
||||||
try:
|
try:
|
||||||
os.kill(oldpid, 0)
|
os.kill(oldpid, 0)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
if e.errno == errno.ESRCH:
|
if e.errno == errno.ESRCH:
|
||||||
os.unlink(_pidname)
|
os.unlink(_pidname)
|
||||||
return # outdated pidfile, ok
|
return # outdated pidfile, ok
|
||||||
@ -156,7 +64,7 @@ def daemonize():
|
|||||||
if os.fork():
|
if os.fork():
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0666)
|
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
|
||||||
try:
|
try:
|
||||||
os.write(outfd, '%d\n' % os.getpid())
|
os.write(outfd, '%d\n' % os.getpid())
|
||||||
finally:
|
finally:
|
||||||
@ -178,44 +86,12 @@ def daemonize():
|
|||||||
def daemon_cleanup():
|
def daemon_cleanup():
|
||||||
try:
|
try:
|
||||||
os.unlink(_pidname)
|
os.unlink(_pidname)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
pf_command_file = None
|
|
||||||
|
|
||||||
def pf_dst(sock):
|
|
||||||
peer = sock.getpeername()
|
|
||||||
proxy = sock.getsockname()
|
|
||||||
|
|
||||||
argv = (sock.family, socket.IPPROTO_TCP, peer[0], peer[1], proxy[0], proxy[1])
|
|
||||||
pf_command_file.write("QUERY_PF_NAT %r,%r,%s,%r,%s,%r\n" % argv)
|
|
||||||
pf_command_file.flush()
|
|
||||||
line = pf_command_file.readline()
|
|
||||||
debug2("QUERY_PF_NAT %r,%r,%s,%r,%s,%r" % argv + ' > ' + line)
|
|
||||||
if line.startswith('QUERY_PF_NAT_SUCCESS '):
|
|
||||||
(ip, port) = line[21:].split(',')
|
|
||||||
return (ip, int(port))
|
|
||||||
|
|
||||||
return sock.getsockname()
|
|
||||||
|
|
||||||
def original_dst(sock):
|
|
||||||
try:
|
|
||||||
SO_ORIGINAL_DST = 80
|
|
||||||
SOCKADDR_MIN = 16
|
|
||||||
sockaddr_in = sock.getsockopt(socket.SOL_IP,
|
|
||||||
SO_ORIGINAL_DST, SOCKADDR_MIN)
|
|
||||||
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
|
|
||||||
assert(socket.htons(proto) == socket.AF_INET)
|
|
||||||
ip = '%d.%d.%d.%d' % (a, b, c, d)
|
|
||||||
return (ip, port)
|
|
||||||
except socket.error, e:
|
|
||||||
if e.args[0] == errno.ENOPROTOOPT:
|
|
||||||
return sock.getsockname()
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
class MultiListener:
|
class MultiListener:
|
||||||
|
|
||||||
@ -230,16 +106,18 @@ class MultiListener:
|
|||||||
self.v4.setsockopt(level, optname, value)
|
self.v4.setsockopt(level, optname, value)
|
||||||
|
|
||||||
def add_handler(self, handlers, callback, method, mux):
|
def add_handler(self, handlers, callback, method, mux):
|
||||||
|
socks = []
|
||||||
if self.v6:
|
if self.v6:
|
||||||
handlers.append(
|
socks.append(self.v6)
|
||||||
Handler(
|
|
||||||
[self.v6],
|
|
||||||
lambda: callback(self.v6, method, mux, handlers)))
|
|
||||||
if self.v4:
|
if self.v4:
|
||||||
handlers.append(
|
socks.append(self.v4)
|
||||||
Handler(
|
|
||||||
[self.v4],
|
handlers.append(
|
||||||
lambda: callback(self.v4, method, mux, handlers)))
|
Handler(
|
||||||
|
socks,
|
||||||
|
lambda sock: callback(sock, method, mux, handlers)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def listen(self, backlog):
|
def listen(self, backlog):
|
||||||
if self.v6:
|
if self.v6:
|
||||||
@ -247,7 +125,7 @@ class MultiListener:
|
|||||||
if self.v4:
|
if self.v4:
|
||||||
try:
|
try:
|
||||||
self.v4.listen(backlog)
|
self.v4.listen(backlog)
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
# on some systems v4 bind will fail if the v6 suceeded,
|
# on some systems v4 bind will fail if the v6 suceeded,
|
||||||
# in this case the v6 socket will receive v4 too.
|
# in this case the v6 socket will receive v4 too.
|
||||||
if e.errno == errno.EADDRINUSE and self.v6:
|
if e.errno == errno.EADDRINUSE and self.v6:
|
||||||
@ -269,28 +147,27 @@ class MultiListener:
|
|||||||
if self.v6:
|
if self.v6:
|
||||||
listenip = self.v6.getsockname()
|
listenip = self.v6.getsockname()
|
||||||
debug1('%s listening on %r.\n' % (what, listenip))
|
debug1('%s listening on %r.\n' % (what, listenip))
|
||||||
|
debug2('%s listening with %r.\n' % (what, self.v6))
|
||||||
if self.v4:
|
if self.v4:
|
||||||
listenip = self.v4.getsockname()
|
listenip = self.v4.getsockname()
|
||||||
debug1('%s listening on %r.\n' % (what, listenip))
|
debug1('%s listening on %r.\n' % (what, listenip))
|
||||||
|
debug2('%s listening with %r.\n' % (what, self.v4))
|
||||||
|
|
||||||
|
|
||||||
class FirewallClient:
|
class FirewallClient:
|
||||||
|
|
||||||
def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude,
|
def __init__(self, method_name):
|
||||||
dnsport_v6, dnsport_v4, method, udp):
|
|
||||||
self.auto_nets = []
|
self.auto_nets = []
|
||||||
self.subnets_include = subnets_include
|
python_path = os.path.dirname(os.path.dirname(__file__))
|
||||||
self.subnets_exclude = subnets_exclude
|
argvbase = ([sys.executable, sys.argv[0]] +
|
||||||
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
|
|
||||||
['-v'] * (helpers.verbose or 0) +
|
['-v'] * (helpers.verbose or 0) +
|
||||||
['--firewall', str(port_v6), str(port_v4),
|
['--method', method_name] +
|
||||||
str(dnsport_v6), str(dnsport_v4),
|
['--firewall'])
|
||||||
method, str(int(udp))])
|
|
||||||
if ssyslog._p:
|
if ssyslog._p:
|
||||||
argvbase += ['--syslog']
|
argvbase += ['--syslog']
|
||||||
argv_tries = [
|
argv_tries = [
|
||||||
['sudo', '-p', '[local sudo] Password: '] + argvbase,
|
['sudo', '-p', '[local sudo] Password: ',
|
||||||
['su', '-c', ' '.join(argvbase)],
|
('PYTHONPATH=%s' % python_path), '--'] + argvbase,
|
||||||
argvbase
|
argvbase
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -313,19 +190,37 @@ class FirewallClient:
|
|||||||
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
||||||
e = None
|
e = None
|
||||||
break
|
break
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
pass
|
pass
|
||||||
self.argv = argv
|
self.argv = argv
|
||||||
s1.close()
|
s1.close()
|
||||||
self.pfile = s2.makefile('wb+')
|
if sys.version_info < (3, 0):
|
||||||
|
# python 2.7
|
||||||
|
self.pfile = s2.makefile('wb+')
|
||||||
|
else:
|
||||||
|
# python 3.5
|
||||||
|
self.pfile = s2.makefile('rwb')
|
||||||
if e:
|
if e:
|
||||||
log('Spawning firewall manager: %r\n' % self.argv)
|
log('Spawning firewall manager: %r\n' % self.argv)
|
||||||
raise Fatal(e)
|
raise Fatal(e)
|
||||||
line = self.pfile.readline()
|
line = self.pfile.readline()
|
||||||
self.check()
|
self.check()
|
||||||
if line[0:5] != 'READY':
|
if line[0:5] != b'READY':
|
||||||
raise Fatal('%r expected READY, got %r' % (self.argv, line))
|
raise Fatal('%r expected READY, got %r' % (self.argv, line))
|
||||||
self.method = line[6:-1]
|
method_name = line[6:-1]
|
||||||
|
self.method = get_method(method_name.decode("ASCII"))
|
||||||
|
self.method.set_firewall(self)
|
||||||
|
|
||||||
|
def setup(self, subnets_include, subnets_exclude, nslist,
|
||||||
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp):
|
||||||
|
self.subnets_include = subnets_include
|
||||||
|
self.subnets_exclude = subnets_exclude
|
||||||
|
self.nslist = nslist
|
||||||
|
self.redirectport_v6 = redirectport_v6
|
||||||
|
self.redirectport_v4 = redirectport_v4
|
||||||
|
self.dnsport_v6 = dnsport_v6
|
||||||
|
self.dnsport_v4 = dnsport_v4
|
||||||
|
self.udp = udp
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
rv = self.p.poll()
|
rv = self.p.poll()
|
||||||
@ -333,22 +228,40 @@ class FirewallClient:
|
|||||||
raise Fatal('%r returned %d' % (self.argv, rv))
|
raise Fatal('%r returned %d' % (self.argv, rv))
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.pfile.write('ROUTES\n')
|
self.pfile.write(b'ROUTES\n')
|
||||||
for (family, ip, width) in self.subnets_include + self.auto_nets:
|
for (family, ip, width) in self.subnets_include + self.auto_nets:
|
||||||
self.pfile.write('%d,%d,0,%s\n' % (family, width, ip))
|
self.pfile.write(b'%d,%d,0,%s\n'
|
||||||
|
% (family, width, ip.encode("ASCII")))
|
||||||
for (family, ip, width) in self.subnets_exclude:
|
for (family, ip, width) in self.subnets_exclude:
|
||||||
self.pfile.write('%d,%d,1,%s\n' % (family, width, ip))
|
self.pfile.write(b'%d,%d,1,%s\n'
|
||||||
self.pfile.write('GO\n')
|
% (family, width, ip.encode("ASCII")))
|
||||||
|
|
||||||
|
self.pfile.write(b'NSLIST\n')
|
||||||
|
for (family, ip) in self.nslist:
|
||||||
|
self.pfile.write(b'%d,%s\n'
|
||||||
|
% (family, ip.encode("ASCII")))
|
||||||
|
|
||||||
|
self.pfile.write(
|
||||||
|
b'PORTS %d,%d,%d,%d\n'
|
||||||
|
% (self.redirectport_v6, self.redirectport_v4,
|
||||||
|
self.dnsport_v6, self.dnsport_v4))
|
||||||
|
|
||||||
|
udp = 0
|
||||||
|
if self.udp:
|
||||||
|
udp = 1
|
||||||
|
|
||||||
|
self.pfile.write(b'GO %d\n' % udp)
|
||||||
self.pfile.flush()
|
self.pfile.flush()
|
||||||
|
|
||||||
line = self.pfile.readline()
|
line = self.pfile.readline()
|
||||||
self.check()
|
self.check()
|
||||||
if line != 'STARTED\n':
|
if line != b'STARTED\n':
|
||||||
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(r'[^-\w]', hostname))
|
assert(not re.search(r'[^-\w]', hostname))
|
||||||
assert(not re.search(r'[^0-9.]', ip))
|
assert(not re.search(r'[^0-9.]', ip))
|
||||||
self.pfile.write('HOST %s,%s\n' % (hostname, ip))
|
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
|
||||||
self.pfile.flush()
|
self.pfile.flush()
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
@ -382,7 +295,7 @@ def onaccept_tcp(listener, method, mux, handlers):
|
|||||||
global _extra_fd
|
global _extra_fd
|
||||||
try:
|
try:
|
||||||
sock, srcip = listener.accept()
|
sock, srcip = listener.accept()
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
|
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
|
||||||
debug1('Rejected incoming connection: too many open files!\n')
|
debug1('Rejected incoming connection: too many open files!\n')
|
||||||
# free up an fd so we can eat the connection
|
# free up an fd so we can eat the connection
|
||||||
@ -395,12 +308,8 @@ def onaccept_tcp(listener, method, mux, handlers):
|
|||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
if method == "tproxy":
|
|
||||||
dstip = sock.getsockname()
|
dstip = method.get_tcp_dstip(sock)
|
||||||
elif method == "pf":
|
|
||||||
dstip = pf_dst(sock)
|
|
||||||
else:
|
|
||||||
dstip = original_dst(sock)
|
|
||||||
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
|
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
|
||||||
dstip[0], dstip[1]))
|
dstip[0], dstip[1]))
|
||||||
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
|
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
|
||||||
@ -412,44 +321,33 @@ def onaccept_tcp(listener, method, mux, handlers):
|
|||||||
log('warning: too many open channels. Discarded connection.\n')
|
log('warning: too many open channels. Discarded connection.\n')
|
||||||
sock.close()
|
sock.close()
|
||||||
return
|
return
|
||||||
mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%s' %
|
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
|
||||||
(sock.family, dstip[0], dstip[1]))
|
(sock.family, dstip[0].encode("ASCII"), dstip[1]))
|
||||||
outwrap = MuxWrapper(mux, chan)
|
outwrap = MuxWrapper(mux, chan)
|
||||||
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
||||||
expire_connections(time.time(), mux)
|
expire_connections(time.time(), mux)
|
||||||
|
|
||||||
|
|
||||||
def udp_done(chan, data, method, family, dstip):
|
def udp_done(chan, data, method, sock, dstip):
|
||||||
(src, srcport, data) = data.split(",", 2)
|
(src, srcport, data) = data.split(",", 2)
|
||||||
srcip = (src, int(srcport))
|
srcip = (src, int(srcport))
|
||||||
debug3('doing send from %r to %r\n' % (srcip, dstip,))
|
debug3('doing send from %r to %r\n' % (srcip, dstip,))
|
||||||
|
method.send_udp(sock, srcip, dstip, data)
|
||||||
try:
|
|
||||||
sender = socket.socket(family, socket.SOCK_DGRAM)
|
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
|
||||||
sender.bind(srcip)
|
|
||||||
sender.sendto(data, dstip)
|
|
||||||
sender.close()
|
|
||||||
except socket.error, e:
|
|
||||||
debug1('-- ignored socket error sending UDP data: %r\n' % e)
|
|
||||||
|
|
||||||
|
|
||||||
def onaccept_udp(listener, method, mux, handlers):
|
def onaccept_udp(listener, method, mux, handlers):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
srcip, dstip, data = recv_udp(listener, 4096)
|
t = method.recv_udp(listener, 4096)
|
||||||
if not dstip:
|
if t is None:
|
||||||
debug1(
|
|
||||||
"-- ignored UDP from %r: "
|
|
||||||
"couldn't determine destination IP address\n" % (srcip,))
|
|
||||||
return
|
return
|
||||||
|
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, timeout = 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(
|
||||||
chan, data, method, listener.family, dstip=srcip)
|
chan, data, method, listener, dstip=srcip)
|
||||||
mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family)
|
mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family)
|
||||||
udp_by_src[srcip] = chan, now + 30
|
udp_by_src[srcip] = chan, now + 30
|
||||||
|
|
||||||
@ -463,27 +361,15 @@ def dns_done(chan, data, method, sock, srcip, dstip, mux):
|
|||||||
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
|
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
|
||||||
del mux.channels[chan]
|
del mux.channels[chan]
|
||||||
del dnsreqs[chan]
|
del dnsreqs[chan]
|
||||||
if method == "tproxy":
|
method.send_udp(sock, srcip, dstip, data)
|
||||||
debug3('doing send from %r to %r\n' % (srcip, dstip,))
|
|
||||||
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
|
||||||
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
|
||||||
sender.bind(srcip)
|
|
||||||
sender.sendto(data, dstip)
|
|
||||||
sender.close()
|
|
||||||
else:
|
|
||||||
debug3('doing sendto %r\n' % (dstip,))
|
|
||||||
sock.sendto(data, dstip)
|
|
||||||
|
|
||||||
|
|
||||||
def ondns(listener, method, mux, handlers):
|
def ondns(listener, method, mux, handlers):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
srcip, dstip, data = recv_udp(listener, 4096)
|
t = method.recv_udp(listener, 4096)
|
||||||
if method == "tproxy" and not dstip:
|
if t is None:
|
||||||
debug1(
|
|
||||||
"-- ignored UDP from %r: "
|
|
||||||
"couldn't determine destination IP address\n" % (srcip,))
|
|
||||||
return
|
return
|
||||||
|
srcip, dstip, data = t
|
||||||
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
|
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
|
||||||
chan = mux.next_channel()
|
chan = mux.next_channel()
|
||||||
dnsreqs[chan] = now + 30
|
dnsreqs[chan] = now + 30
|
||||||
@ -495,8 +381,11 @@ 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, method, seed_hosts, auto_nets,
|
dns_listener, seed_hosts, auto_nets,
|
||||||
syslog, daemon):
|
syslog, daemon):
|
||||||
|
|
||||||
|
method = fw.method
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = 'c : '
|
helpers.logprefix = 'c : '
|
||||||
@ -508,8 +397,8 @@ 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, method=method))
|
options=dict(latency_control=latency_control))
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.EPIPE:
|
if e.args[0] == errno.EPIPE:
|
||||||
raise Fatal("failed to establish ssh session (1)")
|
raise Fatal("failed to establish ssh session (1)")
|
||||||
else:
|
else:
|
||||||
@ -517,17 +406,17 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
mux = Mux(serversock, serversock)
|
mux = Mux(serversock, serversock)
|
||||||
handlers.append(mux)
|
handlers.append(mux)
|
||||||
|
|
||||||
expected = 'SSHUTTLE0001'
|
expected = b'SSHUTTLE0001'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v = 'x'
|
v = 'x'
|
||||||
while v and v != '\0':
|
while v and v != b'\0':
|
||||||
v = serversock.recv(1)
|
v = serversock.recv(1)
|
||||||
v = 'x'
|
v = 'x'
|
||||||
while v and v != '\0':
|
while v and v != b'\0':
|
||||||
v = serversock.recv(1)
|
v = serversock.recv(1)
|
||||||
initstring = serversock.recv(len(expected))
|
initstring = serversock.recv(len(expected))
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.ECONNRESET:
|
if e.args[0] == errno.ECONNRESET:
|
||||||
raise Fatal("failed to establish ssh session (2)")
|
raise Fatal("failed to establish ssh session (2)")
|
||||||
else:
|
else:
|
||||||
@ -541,7 +430,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
raise Fatal('expected server init string %r; got %r'
|
raise Fatal('expected server init string %r; got %r'
|
||||||
% (expected, initstring))
|
% (expected, initstring))
|
||||||
debug1('connected.\n')
|
debug1('connected.\n')
|
||||||
print 'Connected.'
|
print('Connected.')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
if daemon:
|
if daemon:
|
||||||
daemonize()
|
daemonize()
|
||||||
@ -554,7 +443,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
if auto_nets:
|
if auto_nets:
|
||||||
for line in routestr.strip().split('\n'):
|
for line in routestr.strip().split('\n'):
|
||||||
(family, ip, width) = line.split(',', 2)
|
(family, ip, width) = line.split(',', 2)
|
||||||
fw.auto_nets.append((family, ip, int(width)))
|
fw.auto_nets.append((int(family), ip, int(width)))
|
||||||
|
|
||||||
# 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!
|
||||||
@ -595,12 +484,11 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|||||||
ssnet.runonce(handlers, mux)
|
ssnet.runonce(handlers, mux)
|
||||||
if latency_control:
|
if latency_control:
|
||||||
mux.check_fullness()
|
mux.check_fullness()
|
||||||
mux.callback()
|
|
||||||
|
|
||||||
|
|
||||||
def main(listenip_v6, listenip_v4,
|
def main(listenip_v6, listenip_v4,
|
||||||
ssh_cmd, remotename, python, latency_control, dns,
|
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
||||||
method, seed_hosts, auto_nets,
|
method_name, seed_hosts, auto_nets,
|
||||||
subnets_include, subnets_exclude, syslog, daemon, pidfile):
|
subnets_include, subnets_exclude, syslog, daemon, pidfile):
|
||||||
|
|
||||||
if syslog:
|
if syslog:
|
||||||
@ -608,34 +496,32 @@ def main(listenip_v6, listenip_v4,
|
|||||||
if daemon:
|
if daemon:
|
||||||
try:
|
try:
|
||||||
check_daemon(pidfile)
|
check_daemon(pidfile)
|
||||||
except Fatal, e:
|
except Fatal as e:
|
||||||
log("%s\n" % e)
|
log("%s\n" % e)
|
||||||
return 5
|
return 5
|
||||||
debug1('Starting sshuttle proxy.\n')
|
debug1('Starting sshuttle proxy.\n')
|
||||||
|
|
||||||
if recvmsg is not None:
|
fw = FirewallClient(method_name)
|
||||||
debug1("recvmsg %s support enabled.\n" % recvmsg)
|
|
||||||
|
|
||||||
if method == "tproxy":
|
features = fw.method.get_supported_features()
|
||||||
if recvmsg is not None:
|
if listenip_v6 == "auto":
|
||||||
debug1("tproxy UDP support enabled.\n")
|
if features.ipv6:
|
||||||
udp = True
|
listenip_v6 = ('::1', 0)
|
||||||
else:
|
else:
|
||||||
debug1("tproxy UDP support requires recvmsg function.\n")
|
listenip_v6 = None
|
||||||
udp = False
|
|
||||||
if dns and recvmsg is None:
|
if listenip_v4 == "auto":
|
||||||
debug1("tproxy DNS support requires recvmsg function.\n")
|
listenip_v4 = ('127.0.0.1', 0)
|
||||||
dns = False
|
|
||||||
else:
|
udp = features.udp
|
||||||
debug1("UDP support requires tproxy; disabling UDP.\n")
|
debug1("UDP enabled: %r\n" % udp)
|
||||||
udp = False
|
|
||||||
|
|
||||||
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 = xrange(12300, 9000, -1)
|
ports = range(12300, 9000, -1)
|
||||||
|
|
||||||
# search for free ports and try to bind
|
# search for free ports and try to bind
|
||||||
last_e = None
|
last_e = None
|
||||||
@ -680,11 +566,12 @@ def main(listenip_v6, listenip_v4,
|
|||||||
udp_listener.bind(lv6, lv4)
|
udp_listener.bind(lv6, lv4)
|
||||||
bound = True
|
bound = True
|
||||||
break
|
break
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
if e.errno == errno.EADDRINUSE:
|
if e.errno == errno.EADDRINUSE:
|
||||||
last_e = e
|
last_e = e
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
debug2('\n')
|
debug2('\n')
|
||||||
if not bound:
|
if not bound:
|
||||||
assert(last_e)
|
assert(last_e)
|
||||||
@ -695,10 +582,13 @@ def main(listenip_v6, listenip_v4,
|
|||||||
udp_listener.print_listening("UDP redirector")
|
udp_listener.print_listening("UDP redirector")
|
||||||
|
|
||||||
bound = False
|
bound = False
|
||||||
if dns:
|
if dns or nslist:
|
||||||
|
if dns:
|
||||||
|
nslist += resolvconf_nameservers()
|
||||||
|
dns = True
|
||||||
# search for spare port for DNS
|
# search for spare port for DNS
|
||||||
debug2('Binding DNS:')
|
debug2('Binding DNS:')
|
||||||
ports = xrange(12300, 9000, -1)
|
ports = range(12300, 9000, -1)
|
||||||
for port in ports:
|
for port in ports:
|
||||||
debug2(' %d' % port)
|
debug2(' %d' % port)
|
||||||
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
||||||
@ -721,7 +611,7 @@ def main(listenip_v6, listenip_v4,
|
|||||||
dns_listener.bind(lv6, lv4)
|
dns_listener.bind(lv6, lv4)
|
||||||
bound = True
|
bound = True
|
||||||
break
|
break
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
if e.errno == errno.EADDRINUSE:
|
if e.errno == errno.EADDRINUSE:
|
||||||
last_e = e
|
last_e = e
|
||||||
else:
|
else:
|
||||||
@ -736,34 +626,21 @@ def main(listenip_v6, listenip_v4,
|
|||||||
dnsport_v4 = 0
|
dnsport_v4 = 0
|
||||||
dns_listener = None
|
dns_listener = None
|
||||||
|
|
||||||
fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include,
|
fw.method.check_settings(udp, dns)
|
||||||
subnets_exclude, dnsport_v6, dnsport_v4, method, udp)
|
fw.method.setup_tcp_listener(tcp_listener)
|
||||||
|
if udp_listener:
|
||||||
|
fw.method.setup_udp_listener(udp_listener)
|
||||||
|
if dns_listener:
|
||||||
|
fw.method.setup_udp_listener(dns_listener)
|
||||||
|
|
||||||
if fw.method == "tproxy":
|
fw.setup(subnets_include, subnets_exclude, nslist,
|
||||||
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
|
||||||
if udp_listener:
|
udp)
|
||||||
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
|
||||||
if udp_listener.v4 is not None:
|
|
||||||
udp_listener.v4.setsockopt(
|
|
||||||
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
|
||||||
if udp_listener.v6 is not None:
|
|
||||||
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
|
||||||
if dns_listener:
|
|
||||||
dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
|
||||||
if dns_listener.v4 is not None:
|
|
||||||
dns_listener.v4.setsockopt(
|
|
||||||
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
|
||||||
if dns_listener.v6 is not None:
|
|
||||||
dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
|
||||||
|
|
||||||
if fw.method == "pf":
|
|
||||||
global pf_command_file
|
|
||||||
pf_command_file = fw.pfile
|
|
||||||
|
|
||||||
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,
|
||||||
fw.method, seed_hosts, auto_nets, syslog,
|
seed_hosts, auto_nets, syslog,
|
||||||
daemon)
|
daemon)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
231
sshuttle/firewall.py
Normal file
231
sshuttle/firewall.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import errno
|
||||||
|
import socket
|
||||||
|
import signal
|
||||||
|
import sshuttle.ssyslog as ssyslog
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from sshuttle.helpers import debug1, debug2, Fatal
|
||||||
|
from sshuttle.methods import get_auto_method, get_method
|
||||||
|
|
||||||
|
hostmap = {}
|
||||||
|
HOSTSFILE = '/etc/hosts'
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_etc_hosts(port):
|
||||||
|
BAKFILE = '%s.sbak' % HOSTSFILE
|
||||||
|
APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port
|
||||||
|
old_content = ''
|
||||||
|
st = None
|
||||||
|
try:
|
||||||
|
old_content = open(HOSTSFILE).read()
|
||||||
|
st = os.stat(HOSTSFILE)
|
||||||
|
except IOError as e:
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
if old_content.strip() and not os.path.exists(BAKFILE):
|
||||||
|
os.link(HOSTSFILE, BAKFILE)
|
||||||
|
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
||||||
|
f = open(tmpname, 'w')
|
||||||
|
for line in old_content.rstrip().split('\n'):
|
||||||
|
if line.find(APPEND) >= 0:
|
||||||
|
continue
|
||||||
|
f.write('%s\n' % line)
|
||||||
|
for (name, ip) in sorted(hostmap.items()):
|
||||||
|
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
if st:
|
||||||
|
os.chown(tmpname, st.st_uid, st.st_gid)
|
||||||
|
os.chmod(tmpname, st.st_mode)
|
||||||
|
else:
|
||||||
|
os.chown(tmpname, 0, 0)
|
||||||
|
os.chmod(tmpname, 0o644)
|
||||||
|
os.rename(tmpname, HOSTSFILE)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_etc_hosts(port):
|
||||||
|
global hostmap
|
||||||
|
hostmap = {}
|
||||||
|
rewrite_etc_hosts(port)
|
||||||
|
|
||||||
|
|
||||||
|
# Isolate function that needs to be replaced for tests
|
||||||
|
def setup_daemon():
|
||||||
|
if os.getuid() != 0:
|
||||||
|
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
||||||
|
|
||||||
|
# don't disappear if our controlling terminal or stdout/stderr
|
||||||
|
# disappears; we still have to clean up.
|
||||||
|
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||||
|
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||||
|
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
|
||||||
|
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
||||||
|
# I'll die automatically.
|
||||||
|
os.setsid()
|
||||||
|
|
||||||
|
# because of limitations of the 'su' command, the *real* stdin/stdout
|
||||||
|
# are both attached to stdout initially. Clone stdout into stdin so we
|
||||||
|
# can read from it.
|
||||||
|
os.dup2(1, 0)
|
||||||
|
|
||||||
|
return sys.stdin, sys.stdout
|
||||||
|
|
||||||
|
|
||||||
|
# This is some voodoo for setting up the kernel's transparent
|
||||||
|
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
||||||
|
# otherwise we delete it, then make them from scratch.
|
||||||
|
#
|
||||||
|
# This code is supposed to clean up after itself by deleting its rules on
|
||||||
|
# exit. In case that fails, it's not the end of the world; future runs will
|
||||||
|
# supercede it in the transproxy list, at least, so the leftover rules
|
||||||
|
# are hopefully harmless.
|
||||||
|
def main(method_name, syslog):
|
||||||
|
stdin, stdout = setup_daemon()
|
||||||
|
|
||||||
|
if method_name == "auto":
|
||||||
|
method = get_auto_method()
|
||||||
|
else:
|
||||||
|
method = get_method(method_name)
|
||||||
|
|
||||||
|
if syslog:
|
||||||
|
ssyslog.start_syslog()
|
||||||
|
ssyslog.stderr_to_syslog()
|
||||||
|
|
||||||
|
debug1('firewall manager ready method name %s.\n' % method.name)
|
||||||
|
stdout.write('READY %s\n' % method.name)
|
||||||
|
stdout.flush()
|
||||||
|
|
||||||
|
# we wait until we get some input before creating the rules. That way,
|
||||||
|
# sshuttle can launch us as early as possible (and get sudo password
|
||||||
|
# authentication as early in the startup process as possible).
|
||||||
|
line = stdin.readline(128)
|
||||||
|
if not line:
|
||||||
|
return # parent died; nothing to do
|
||||||
|
|
||||||
|
subnets = []
|
||||||
|
if line != 'ROUTES\n':
|
||||||
|
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
||||||
|
while 1:
|
||||||
|
line = stdin.readline(128)
|
||||||
|
if not line:
|
||||||
|
raise Fatal('firewall: expected route but got %r' % line)
|
||||||
|
elif line.startswith("NSLIST\n"):
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
(family, width, exclude, ip) = line.strip().split(',', 3)
|
||||||
|
except:
|
||||||
|
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
|
||||||
|
subnets.append((int(family), int(width), bool(int(exclude)), ip))
|
||||||
|
debug2('Got subnets: %r\n' % subnets)
|
||||||
|
|
||||||
|
nslist = []
|
||||||
|
if line != 'NSLIST\n':
|
||||||
|
raise Fatal('firewall: expected NSLIST but got %r' % line)
|
||||||
|
while 1:
|
||||||
|
line = stdin.readline(128)
|
||||||
|
if not line:
|
||||||
|
raise Fatal('firewall: expected nslist but got %r' % line)
|
||||||
|
elif line.startswith("PORTS "):
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
(family, ip) = line.strip().split(',', 1)
|
||||||
|
except:
|
||||||
|
raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
|
||||||
|
nslist.append((int(family), ip))
|
||||||
|
debug2('Got partial nslist: %r\n' % nslist)
|
||||||
|
debug2('Got nslist: %r\n' % nslist)
|
||||||
|
|
||||||
|
if not line.startswith('PORTS '):
|
||||||
|
raise Fatal('firewall: expected PORTS but got %r' % line)
|
||||||
|
_, _, ports = line.partition(" ")
|
||||||
|
ports = ports.split(",")
|
||||||
|
if len(ports) != 4:
|
||||||
|
raise Fatal('firewall: expected 4 ports but got %n' % len(ports))
|
||||||
|
port_v6 = int(ports[0])
|
||||||
|
port_v4 = int(ports[1])
|
||||||
|
dnsport_v6 = int(ports[2])
|
||||||
|
dnsport_v4 = int(ports[3])
|
||||||
|
|
||||||
|
assert(port_v6 >= 0)
|
||||||
|
assert(port_v6 <= 65535)
|
||||||
|
assert(port_v4 >= 0)
|
||||||
|
assert(port_v4 <= 65535)
|
||||||
|
assert(dnsport_v6 >= 0)
|
||||||
|
assert(dnsport_v6 <= 65535)
|
||||||
|
assert(dnsport_v4 >= 0)
|
||||||
|
assert(dnsport_v4 <= 65535)
|
||||||
|
|
||||||
|
debug2('Got ports: %d,%d,%d,%d\n'
|
||||||
|
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
|
||||||
|
|
||||||
|
line = stdin.readline(128)
|
||||||
|
if not line:
|
||||||
|
raise Fatal('firewall: expected GO but got %r' % line)
|
||||||
|
elif not line.startswith("GO "):
|
||||||
|
raise Fatal('firewall: expected GO but got %r' % line)
|
||||||
|
|
||||||
|
_, _, udp = line.partition(" ")
|
||||||
|
udp = bool(int(udp))
|
||||||
|
debug2('Got udp: %r\n' % udp)
|
||||||
|
|
||||||
|
try:
|
||||||
|
do_wait = None
|
||||||
|
debug1('firewall manager: starting transproxy.\n')
|
||||||
|
|
||||||
|
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
||||||
|
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
||||||
|
if port_v6 > 0:
|
||||||
|
do_wait = method.setup_firewall(
|
||||||
|
port_v6, dnsport_v6, nslist_v6,
|
||||||
|
socket.AF_INET6, subnets_v6, udp)
|
||||||
|
elif len(subnets_v6) > 0:
|
||||||
|
debug1("IPv6 subnets defined but IPv6 disabled\n")
|
||||||
|
|
||||||
|
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
||||||
|
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
|
||||||
|
if port_v4 > 0:
|
||||||
|
do_wait = method.setup_firewall(
|
||||||
|
port_v4, dnsport_v4, nslist_v4,
|
||||||
|
socket.AF_INET, subnets_v4, udp)
|
||||||
|
elif len(subnets_v4) > 0:
|
||||||
|
debug1('IPv4 subnets defined but IPv4 disabled\n')
|
||||||
|
|
||||||
|
stdout.write('STARTED\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
stdout.flush()
|
||||||
|
except IOError:
|
||||||
|
# the parent process died for some reason; he's surely been loud
|
||||||
|
# enough, so no reason to report another error
|
||||||
|
return
|
||||||
|
|
||||||
|
# Now we wait until EOF or any other kind of exception. We need
|
||||||
|
# to stay running so that we don't need a *second* password
|
||||||
|
# authentication at shutdown time - that cleanup is important!
|
||||||
|
while 1:
|
||||||
|
if do_wait is not None:
|
||||||
|
do_wait()
|
||||||
|
line = stdin.readline(128)
|
||||||
|
if line.startswith('HOST '):
|
||||||
|
(name, ip) = line[5:].strip().split(',', 1)
|
||||||
|
hostmap[name] = ip
|
||||||
|
rewrite_etc_hosts(port_v6 or port_v4)
|
||||||
|
elif line:
|
||||||
|
if not method.firewall_command(line):
|
||||||
|
raise Fatal('expected EOF, got %r' % line)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
debug1('firewall manager: undoing changes.\n')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if port_v6:
|
||||||
|
method.setup_firewall(port_v6, 0, [], socket.AF_INET6, [], udp)
|
||||||
|
if port_v4:
|
||||||
|
method.setup_firewall(port_v4, 0, [], socket.AF_INET, [], udp)
|
||||||
|
restore_etc_hosts(port_v6 or port_v4)
|
@ -36,22 +36,12 @@ class Fatal(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def list_contains_any(l, sub):
|
|
||||||
for i in sub:
|
|
||||||
if i in l:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def resolvconf_nameservers():
|
def resolvconf_nameservers():
|
||||||
l = []
|
l = []
|
||||||
for line in open('/etc/resolv.conf'):
|
for line in open('/etc/resolv.conf'):
|
||||||
words = line.lower().split()
|
words = line.lower().split()
|
||||||
if len(words) >= 2 and words[0] == 'nameserver':
|
if len(words) >= 2 and words[0] == 'nameserver':
|
||||||
if ':' in words[1]:
|
l.append(family_ip_tuple(words[1]))
|
||||||
l.append((socket.AF_INET6, words[1]))
|
|
||||||
else:
|
|
||||||
l.append((socket.AF_INET, words[1]))
|
|
||||||
return l
|
return l
|
||||||
|
|
||||||
|
|
||||||
@ -72,7 +62,7 @@ def islocal(ip, family):
|
|||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
sock.bind((ip, 0))
|
sock.bind((ip, 0))
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
if e.args[0] == errno.EADDRNOTAVAIL:
|
if e.args[0] == errno.EADDRNOTAVAIL:
|
||||||
return False # not a local IP
|
return False # not a local IP
|
||||||
else:
|
else:
|
||||||
@ -82,6 +72,13 @@ def islocal(ip, family):
|
|||||||
return True # it's a local IP, or there would have been an error
|
return True # it's a local IP, or there would have been an error
|
||||||
|
|
||||||
|
|
||||||
|
def family_ip_tuple(ip):
|
||||||
|
if ':' in ip:
|
||||||
|
return (socket.AF_INET6, ip)
|
||||||
|
else:
|
||||||
|
return (socket.AF_INET, ip)
|
||||||
|
|
||||||
|
|
||||||
def family_to_string(family):
|
def family_to_string(family):
|
||||||
if family == socket.AF_INET6:
|
if family == socket.AF_INET6:
|
||||||
return "AF_INET6"
|
return "AF_INET6"
|
@ -5,10 +5,10 @@ import select
|
|||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
if not globals().get('skip_imports'):
|
|
||||||
import compat.ssubprocess as ssubprocess
|
import subprocess as ssubprocess
|
||||||
import helpers
|
import sshuttle.helpers as helpers
|
||||||
from helpers import log, debug1, debug2, debug3
|
from sshuttle.helpers import log, debug1, debug2, debug3
|
||||||
|
|
||||||
POLL_TIME = 60 * 15
|
POLL_TIME = 60 * 15
|
||||||
NETSTAT_POLL_TIME = 30
|
NETSTAT_POLL_TIME = 30
|
||||||
@ -21,7 +21,7 @@ hostnames = {}
|
|||||||
queue = {}
|
queue = {}
|
||||||
try:
|
try:
|
||||||
null = open('/dev/null', 'wb')
|
null = open('/dev/null', 'wb')
|
||||||
except IOError, e:
|
except IOError as e:
|
||||||
log('warning: %s\n' % e)
|
log('warning: %s\n' % e)
|
||||||
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ def write_host_cache():
|
|||||||
def read_host_cache():
|
def read_host_cache():
|
||||||
try:
|
try:
|
||||||
f = open(CACHEFILE)
|
f = open(CACHEFILE)
|
||||||
except IOError, e:
|
except IOError as e:
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@ -122,7 +122,7 @@ def _check_netstat():
|
|||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||||
content = p.stdout.read()
|
content = p.stdout.read()
|
||||||
p.wait()
|
p.wait()
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ def _check_smb(hostname):
|
|||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||||
lines = p.stdout.readlines()
|
lines = p.stdout.readlines()
|
||||||
p.wait()
|
p.wait()
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
_smb_ok = False
|
_smb_ok = False
|
||||||
return
|
return
|
||||||
@ -199,7 +199,7 @@ def _check_nmb(hostname, is_workgroup, is_master):
|
|||||||
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
|
||||||
lines = p.stdout.readlines()
|
lines = p.stdout.readlines()
|
||||||
rv = p.wait()
|
rv = p.wait()
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
log('%r failed: %r\n' % (argv, e))
|
log('%r failed: %r\n' % (argv, e))
|
||||||
_nmb_ok = False
|
_nmb_ok = False
|
||||||
return
|
return
|
||||||
@ -267,7 +267,7 @@ def hw_main(seed_hosts):
|
|||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for t, last_polled in queue.items():
|
for t, last_polled in list(queue.items()):
|
||||||
(op, args) = t
|
(op, args) = t
|
||||||
if not _stdin_still_ok(0):
|
if not _stdin_still_ok(0):
|
||||||
break
|
break
|
62
sshuttle/linux.py
Normal file
62
sshuttle/linux.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import socket
|
||||||
|
import subprocess as ssubprocess
|
||||||
|
from sshuttle.helpers import log, debug1, Fatal, family_to_string
|
||||||
|
|
||||||
|
|
||||||
|
def nonfatal(func, *args):
|
||||||
|
try:
|
||||||
|
func(*args)
|
||||||
|
except Fatal as e:
|
||||||
|
log('error: %s\n' % e)
|
||||||
|
|
||||||
|
|
||||||
|
def ipt_chain_exists(family, table, name):
|
||||||
|
if family == socket.AF_INET6:
|
||||||
|
cmd = 'ip6tables'
|
||||||
|
elif family == socket.AF_INET:
|
||||||
|
cmd = 'iptables'
|
||||||
|
else:
|
||||||
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
|
argv = [cmd, '-t', table, '-nL']
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||||
|
for line in p.stdout:
|
||||||
|
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
|
||||||
|
return True
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
def ipt(family, table, *args):
|
||||||
|
if family == socket.AF_INET6:
|
||||||
|
argv = ['ip6tables', '-t', table] + list(args)
|
||||||
|
elif family == socket.AF_INET:
|
||||||
|
argv = ['iptables', '-t', table] + list(args)
|
||||||
|
else:
|
||||||
|
raise Exception('Unsupported family "%s"' % family_to_string(family))
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
rv = ssubprocess.call(argv)
|
||||||
|
if rv:
|
||||||
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
|
_no_ttl_module = False
|
||||||
|
|
||||||
|
|
||||||
|
def ipt_ttl(family, *args):
|
||||||
|
global _no_ttl_module
|
||||||
|
if not _no_ttl_module:
|
||||||
|
# we avoid infinite loops by generating server-side connections
|
||||||
|
# with ttl 42. This makes the client side not recapture those
|
||||||
|
# connections, in case client == server.
|
||||||
|
try:
|
||||||
|
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
|
||||||
|
ipt(family, *argsplus)
|
||||||
|
except Fatal:
|
||||||
|
ipt(family, *args)
|
||||||
|
# we only get here if the non-ttl attempt succeeds
|
||||||
|
log('sshuttle: warning: your iptables is missing '
|
||||||
|
'the ttl module.\n')
|
||||||
|
_no_ttl_module = True
|
||||||
|
else:
|
||||||
|
ipt(family, *args)
|
99
sshuttle/methods/__init__.py
Normal file
99
sshuttle/methods/__init__.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import errno
|
||||||
|
from sshuttle.helpers import Fatal, debug3
|
||||||
|
|
||||||
|
|
||||||
|
def original_dst(sock):
|
||||||
|
try:
|
||||||
|
SO_ORIGINAL_DST = 80
|
||||||
|
SOCKADDR_MIN = 16
|
||||||
|
sockaddr_in = sock.getsockopt(socket.SOL_IP,
|
||||||
|
SO_ORIGINAL_DST, SOCKADDR_MIN)
|
||||||
|
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
|
||||||
|
# FIXME: decoding is IPv4 only.
|
||||||
|
assert(socket.htons(proto) == socket.AF_INET)
|
||||||
|
ip = '%d.%d.%d.%d' % (a, b, c, d)
|
||||||
|
return (ip, port)
|
||||||
|
except socket.error as e:
|
||||||
|
if e.args[0] == errno.ENOPROTOOPT:
|
||||||
|
return sock.getsockname()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class Features(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMethod(object):
|
||||||
|
def __init__(self, name):
|
||||||
|
self.firewall = None
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def set_firewall(self, firewall):
|
||||||
|
self.firewall = firewall
|
||||||
|
|
||||||
|
def get_supported_features(self):
|
||||||
|
result = Features()
|
||||||
|
result.ipv6 = False
|
||||||
|
result.udp = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_tcp_dstip(self, sock):
|
||||||
|
return original_dst(sock)
|
||||||
|
|
||||||
|
def recv_udp(self, udp_listener, bufsize):
|
||||||
|
debug3('Accept UDP using recvfrom.\n')
|
||||||
|
data, srcip = udp_listener.recvfrom(bufsize)
|
||||||
|
return (srcip, None, data)
|
||||||
|
|
||||||
|
def send_udp(self, sock, srcip, dstip, data):
|
||||||
|
if srcip is not None:
|
||||||
|
Fatal("Method %s send_udp does not support setting srcip to %r"
|
||||||
|
% (self.name, srcip))
|
||||||
|
sock.sendto(data, dstip)
|
||||||
|
|
||||||
|
def setup_tcp_listener(self, tcp_listener):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup_udp_listener(self, udp_listener):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_settings(self, udp, dns):
|
||||||
|
if udp:
|
||||||
|
Fatal("UDP support not supported with method %s.\n" % self.name)
|
||||||
|
|
||||||
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def firewall_command(self, line):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _program_exists(name):
|
||||||
|
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
||||||
|
for p in paths:
|
||||||
|
fn = '%s/%s' % (p, name)
|
||||||
|
if os.path.exists(fn):
|
||||||
|
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def get_method(method_name):
|
||||||
|
module = importlib.import_module("sshuttle.methods.%s" % method_name)
|
||||||
|
return module.Method(method_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_auto_method():
|
||||||
|
if _program_exists('ipfw'):
|
||||||
|
method_name = "ipfw"
|
||||||
|
elif _program_exists('iptables'):
|
||||||
|
method_name = "nat"
|
||||||
|
elif _program_exists('pfctl'):
|
||||||
|
method_name = "pf"
|
||||||
|
else:
|
||||||
|
raise Fatal(
|
||||||
|
"can't find either ipfw, iptables or pfctl; check your PATH")
|
||||||
|
|
||||||
|
return get_method(method_name)
|
237
sshuttle/methods/ipfw.py
Normal file
237
sshuttle/methods/ipfw.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import sys
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import subprocess as ssubprocess
|
||||||
|
from sshuttle.helpers import log, debug1, debug3, islocal, \
|
||||||
|
Fatal, family_to_string
|
||||||
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
|
# python doesn't have a definition for this
|
||||||
|
IPPROTO_DIVERT = 254
|
||||||
|
|
||||||
|
|
||||||
|
def ipfw_rule_exists(n):
|
||||||
|
argv = ['ipfw', 'list']
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||||
|
found = False
|
||||||
|
for line in p.stdout:
|
||||||
|
if line.startswith('%05d ' % n):
|
||||||
|
if not ('ipttl 42' in line
|
||||||
|
or ('skipto %d' % (n + 1)) 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]
|
||||||
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
|
||||||
|
for line in p.stdout:
|
||||||
|
assert(line[-1] == '\n')
|
||||||
|
(k, v) = line[:-1].split(': ', 1)
|
||||||
|
_oldctls[k] = v
|
||||||
|
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 _udp_unpack(p):
|
||||||
|
src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0])
|
||||||
|
dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0])
|
||||||
|
return src, dst
|
||||||
|
|
||||||
|
|
||||||
|
def _udp_repack(p, src, dst):
|
||||||
|
addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0])
|
||||||
|
ports = struct.pack('!HH', src[1], dst[1])
|
||||||
|
return p[:12] + addrs + ports + p[24:]
|
||||||
|
|
||||||
|
|
||||||
|
_real_dns_server = [None]
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_diversion(divertsock, dnsport):
|
||||||
|
p, tag = divertsock.recvfrom(4096)
|
||||||
|
src, dst = _udp_unpack(p)
|
||||||
|
debug3('got diverted packet from %r to %r\n' % (src, dst))
|
||||||
|
if dst[1] == 53:
|
||||||
|
# outgoing DNS
|
||||||
|
debug3('...packet is a DNS request.\n')
|
||||||
|
_real_dns_server[0] = dst
|
||||||
|
dst = ('127.0.0.1', dnsport)
|
||||||
|
elif src[1] == dnsport:
|
||||||
|
if islocal(src[0], divertsock.family):
|
||||||
|
debug3('...packet is a DNS response.\n')
|
||||||
|
src = _real_dns_server[0]
|
||||||
|
else:
|
||||||
|
log('weird?! unexpected divert from %r to %r\n' % (src, dst))
|
||||||
|
assert(0)
|
||||||
|
newp = _udp_repack(p, src, dst)
|
||||||
|
divertsock.sendto(newp, tag)
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
||||||
|
# IPv6 not supported
|
||||||
|
if family not in [socket.AF_INET, ]:
|
||||||
|
raise Exception(
|
||||||
|
'Address family "%s" unsupported by ipfw method_name'
|
||||||
|
% family_to_string(family))
|
||||||
|
if udp:
|
||||||
|
raise Exception("UDP not supported by ipfw method_name")
|
||||||
|
|
||||||
|
sport = str(port)
|
||||||
|
xsport = str(port + 1)
|
||||||
|
|
||||||
|
# cleanup any existing rules
|
||||||
|
if ipfw_rule_exists(port):
|
||||||
|
ipfw('delete', sport)
|
||||||
|
|
||||||
|
while _changedctls:
|
||||||
|
name = _changedctls.pop()
|
||||||
|
oldval = _oldctls[name]
|
||||||
|
_sysctl_set(name, oldval)
|
||||||
|
|
||||||
|
if subnets or dnsport:
|
||||||
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||||
|
changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
|
||||||
|
if changed:
|
||||||
|
log("\n"
|
||||||
|
" WARNING: ONE-TIME NETWORK DISRUPTION:\n"
|
||||||
|
" =====================================\n"
|
||||||
|
"sshuttle has changed a MacOS kernel setting to work around\n"
|
||||||
|
"a bug in MacOS 10.6. This will cause your network to drop\n"
|
||||||
|
"within 5-10 minutes unless you restart your network\n"
|
||||||
|
"interface (change wireless networks or unplug/plug the\n"
|
||||||
|
"ethernet port) NOW, then restart sshuttle. The fix is\n"
|
||||||
|
"permanent; you only have to do this once.\n\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ipfw('add', sport, 'check-state', 'ip',
|
||||||
|
'from', 'any', 'to', 'any')
|
||||||
|
|
||||||
|
if subnets:
|
||||||
|
# create new subnet entries
|
||||||
|
for f, swidth, sexclude, snet \
|
||||||
|
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||||
|
if sexclude:
|
||||||
|
ipfw('add', sport, 'skipto', xsport,
|
||||||
|
'tcp',
|
||||||
|
'from', 'any', 'to', '%s/%s' % (snet, swidth))
|
||||||
|
else:
|
||||||
|
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
||||||
|
'tcp',
|
||||||
|
'from', 'any', 'to', '%s/%s' % (snet, swidth),
|
||||||
|
'not', 'ipttl', '42', 'keep-state', 'setup')
|
||||||
|
|
||||||
|
# This part is much crazier than it is on Linux, because MacOS (at
|
||||||
|
# least 10.6, and probably other versions, and maybe FreeBSD too)
|
||||||
|
# doesn't correctly fixup the dstip/dstport for UDP packets when it
|
||||||
|
# puts them through a 'fwd' rule. It also doesn't fixup the
|
||||||
|
# srcip/srcport in the response packet. In Linux iptables, all that
|
||||||
|
# happens magically for us, so we just redirect the packets and relax.
|
||||||
|
#
|
||||||
|
# On MacOS, we have to fix the ports ourselves. For that, we use a
|
||||||
|
# 'divert' socket, which receives raw packets and lets us mangle them.
|
||||||
|
#
|
||||||
|
# Here's how it works. Let's say the local DNS server is 1.1.1.1:53,
|
||||||
|
# and the remote DNS server is 2.2.2.2:53, and the local transproxy
|
||||||
|
# port is 10.0.0.1:12300, and a client machine is making a request from
|
||||||
|
# 10.0.0.5:9999. We see a packet like this:
|
||||||
|
# 10.0.0.5:9999 -> 1.1.1.1:53
|
||||||
|
# Since the destip:port matches one of our local nameservers, it will
|
||||||
|
# match a 'fwd' rule, thus grabbing it on the local machine. However,
|
||||||
|
# the local kernel will then see a packet addressed to *:53 and not
|
||||||
|
# know what to do with it; there's nobody listening on port 53. Thus,
|
||||||
|
# we divert it, rewriting it into this:
|
||||||
|
# 10.0.0.5:9999 -> 10.0.0.1:12300
|
||||||
|
# This gets proxied out to the server, which sends it to 2.2.2.2:53,
|
||||||
|
# and the answer comes back, and the proxy sends it back out like this:
|
||||||
|
# 10.0.0.1:12300 -> 10.0.0.5:9999
|
||||||
|
# But that's wrong! The original machine expected an answer from
|
||||||
|
# 1.1.1.1:53, so we have to divert the *answer* and rewrite it:
|
||||||
|
# 1.1.1.1:53 -> 10.0.0.5:9999
|
||||||
|
#
|
||||||
|
# See? Easy stuff.
|
||||||
|
if dnsport:
|
||||||
|
divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
|
||||||
|
IPPROTO_DIVERT)
|
||||||
|
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
|
||||||
|
|
||||||
|
for f, ip in [i for i in nslist if i[0] == family]:
|
||||||
|
# relabel and then catch outgoing DNS requests
|
||||||
|
ipfw('add', sport, 'divert', sport,
|
||||||
|
'udp',
|
||||||
|
'from', 'any', 'to', '%s/32' % ip, '53',
|
||||||
|
'not', 'ipttl', '42')
|
||||||
|
# relabel DNS responses
|
||||||
|
ipfw('add', sport, 'divert', sport,
|
||||||
|
'udp',
|
||||||
|
'from', 'any', str(dnsport), 'to', 'any',
|
||||||
|
'not', 'ipttl', '42')
|
||||||
|
|
||||||
|
def do_wait():
|
||||||
|
while 1:
|
||||||
|
r, w, x = select.select([sys.stdin, divertsock], [], [])
|
||||||
|
if divertsock in r:
|
||||||
|
_handle_diversion(divertsock, dnsport)
|
||||||
|
if sys.stdin in r:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
do_wait = None
|
||||||
|
|
||||||
|
return do_wait
|
70
sshuttle/methods/nat.py
Normal file
70
sshuttle/methods/nat.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import socket
|
||||||
|
from sshuttle.helpers import family_to_string
|
||||||
|
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, 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):
|
||||||
|
# only ipv4 supported with NAT
|
||||||
|
if family != socket.AF_INET:
|
||||||
|
raise Exception(
|
||||||
|
'Address family "%s" unsupported by nat method_name'
|
||||||
|
% family_to_string(family))
|
||||||
|
if udp:
|
||||||
|
raise Exception("UDP not supported by nat method_name")
|
||||||
|
|
||||||
|
table = "nat"
|
||||||
|
|
||||||
|
def _ipt(*args):
|
||||||
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
|
def _ipt_ttl(*args):
|
||||||
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
|
chain = 'sshuttle-%s' % port
|
||||||
|
|
||||||
|
# basic cleanup/setup of chains
|
||||||
|
if ipt_chain_exists(family, table, chain):
|
||||||
|
nonfatal(_ipt, '-D', 'OUTPUT', '-j', chain)
|
||||||
|
nonfatal(_ipt, '-D', 'PREROUTING', '-j', chain)
|
||||||
|
nonfatal(_ipt, '-F', chain)
|
||||||
|
_ipt('-X', chain)
|
||||||
|
|
||||||
|
if subnets or dnsport:
|
||||||
|
_ipt('-N', chain)
|
||||||
|
_ipt('-F', chain)
|
||||||
|
_ipt('-I', 'OUTPUT', '1', '-j', chain)
|
||||||
|
_ipt('-I', 'PREROUTING', '1', '-j', chain)
|
||||||
|
|
||||||
|
if subnets:
|
||||||
|
# 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:
|
||||||
|
_ipt('-A', chain, '-j', 'RETURN',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-p', 'tcp')
|
||||||
|
else:
|
||||||
|
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-p', 'tcp',
|
||||||
|
'--to-ports', str(port))
|
||||||
|
|
||||||
|
if dnsport:
|
||||||
|
for f, ip in [i for i in nslist if i[0] == family]:
|
||||||
|
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
|
'--dest', '%s/32' % ip,
|
||||||
|
'-p', 'udp',
|
||||||
|
'--dport', '53',
|
||||||
|
'--to-ports', str(dnsport))
|
246
sshuttle/methods/pf.py
Normal file
246
sshuttle/methods/pf.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import subprocess as ssubprocess
|
||||||
|
from fcntl import ioctl
|
||||||
|
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
|
||||||
|
sizeof, addressof, memmove
|
||||||
|
from sshuttle.helpers import debug1, debug2, Fatal, family_to_string
|
||||||
|
from sshuttle.methods import BaseMethod
|
||||||
|
|
||||||
|
|
||||||
|
def pfctl(args, stdin=None):
|
||||||
|
argv = ['pfctl'] + list(args.split(" "))
|
||||||
|
debug1('>> %s\n' % ' '.join(argv))
|
||||||
|
|
||||||
|
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
|
||||||
|
stdout=ssubprocess.PIPE,
|
||||||
|
stderr=ssubprocess.PIPE)
|
||||||
|
o = p.communicate(stdin)
|
||||||
|
if p.returncode:
|
||||||
|
raise Fatal('%r returned %d' % (argv, p.returncode))
|
||||||
|
|
||||||
|
return o
|
||||||
|
|
||||||
|
_pf_context = {'started_by_sshuttle': False, 'Xtoken': ''}
|
||||||
|
|
||||||
|
|
||||||
|
# This are some classes and functions used to support pf in yosemite.
|
||||||
|
class pf_state_xport(Union):
|
||||||
|
_fields_ = [("port", c_uint16),
|
||||||
|
("call_id", c_uint16),
|
||||||
|
("spi", c_uint32)]
|
||||||
|
|
||||||
|
|
||||||
|
class pf_addr(Structure):
|
||||||
|
|
||||||
|
class _pfa(Union):
|
||||||
|
_fields_ = [("v4", c_uint32), # struct in_addr
|
||||||
|
("v6", c_uint32 * 4), # struct in6_addr
|
||||||
|
("addr8", c_uint8 * 16),
|
||||||
|
("addr16", c_uint16 * 8),
|
||||||
|
("addr32", c_uint32 * 4)]
|
||||||
|
|
||||||
|
_fields_ = [("pfa", _pfa)]
|
||||||
|
_anonymous_ = ("pfa",)
|
||||||
|
|
||||||
|
|
||||||
|
class pfioc_natlook(Structure):
|
||||||
|
_fields_ = [("saddr", pf_addr),
|
||||||
|
("daddr", pf_addr),
|
||||||
|
("rsaddr", pf_addr),
|
||||||
|
("rdaddr", pf_addr),
|
||||||
|
("sxport", pf_state_xport),
|
||||||
|
("dxport", pf_state_xport),
|
||||||
|
("rsxport", pf_state_xport),
|
||||||
|
("rdxport", pf_state_xport),
|
||||||
|
("af", c_uint8), # sa_family_t
|
||||||
|
("proto", c_uint8),
|
||||||
|
("proto_variant", c_uint8),
|
||||||
|
("direction", c_uint8)]
|
||||||
|
|
||||||
|
pfioc_rule = c_char * 3104 # sizeof(struct pfioc_rule)
|
||||||
|
|
||||||
|
pfioc_pooladdr = c_char * 1136 # sizeof(struct pfioc_pooladdr)
|
||||||
|
|
||||||
|
MAXPATHLEN = 1024
|
||||||
|
|
||||||
|
DIOCNATLOOK = ((0x40000000 | 0x80000000) | (
|
||||||
|
(sizeof(pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23))
|
||||||
|
DIOCCHANGERULE = ((0x40000000 | 0x80000000) | (
|
||||||
|
(sizeof(pfioc_rule) & 0x1fff) << 16) | ((ord('D')) << 8) | (26))
|
||||||
|
DIOCBEGINADDRS = ((0x40000000 | 0x80000000) | (
|
||||||
|
(sizeof(pfioc_pooladdr) & 0x1fff) << 16) | ((ord('D')) << 8) | (51))
|
||||||
|
|
||||||
|
PF_CHANGE_ADD_TAIL = 2
|
||||||
|
PF_CHANGE_GET_TICKET = 6
|
||||||
|
|
||||||
|
PF_PASS = 0
|
||||||
|
PF_RDR = 8
|
||||||
|
|
||||||
|
PF_OUT = 2
|
||||||
|
|
||||||
|
_pf_fd = None
|
||||||
|
|
||||||
|
|
||||||
|
def pf_get_dev():
|
||||||
|
global _pf_fd
|
||||||
|
if _pf_fd is None:
|
||||||
|
_pf_fd = os.open('/dev/pf', os.O_RDWR)
|
||||||
|
|
||||||
|
return _pf_fd
|
||||||
|
|
||||||
|
|
||||||
|
def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
|
||||||
|
[proto, family, src_port, dst_port] = [
|
||||||
|
int(v) for v in [proto, family, src_port, dst_port]]
|
||||||
|
|
||||||
|
packed_src_ip = socket.inet_pton(family, src_ip)
|
||||||
|
packed_dst_ip = socket.inet_pton(family, dst_ip)
|
||||||
|
|
||||||
|
assert len(packed_src_ip) == len(packed_dst_ip)
|
||||||
|
length = len(packed_src_ip)
|
||||||
|
|
||||||
|
pnl = pfioc_natlook()
|
||||||
|
pnl.proto = proto
|
||||||
|
pnl.direction = PF_OUT
|
||||||
|
pnl.af = family
|
||||||
|
memmove(addressof(pnl.saddr), packed_src_ip, length)
|
||||||
|
pnl.sxport.port = socket.htons(src_port)
|
||||||
|
memmove(addressof(pnl.daddr), packed_dst_ip, length)
|
||||||
|
pnl.dxport.port = socket.htons(dst_port)
|
||||||
|
|
||||||
|
ioctl(pf_get_dev(), DIOCNATLOOK,
|
||||||
|
(c_char * sizeof(pnl)).from_address(addressof(pnl)))
|
||||||
|
|
||||||
|
ip = socket.inet_ntop(
|
||||||
|
pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw)
|
||||||
|
port = socket.ntohs(pnl.rdxport.port)
|
||||||
|
return (ip, port)
|
||||||
|
|
||||||
|
|
||||||
|
def pf_add_anchor_rule(type, name):
|
||||||
|
ACTION_OFFSET = 0
|
||||||
|
POOL_TICKET_OFFSET = 8
|
||||||
|
ANCHOR_CALL_OFFSET = 1040
|
||||||
|
RULE_ACTION_OFFSET = 3068
|
||||||
|
|
||||||
|
pr = pfioc_rule()
|
||||||
|
ppa = pfioc_pooladdr()
|
||||||
|
|
||||||
|
ioctl(pf_get_dev(), DIOCBEGINADDRS, ppa)
|
||||||
|
|
||||||
|
memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) # pool_ticket
|
||||||
|
memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name,
|
||||||
|
min(MAXPATHLEN, len(name))) # anchor_call = name
|
||||||
|
memmove(addressof(pr) + RULE_ACTION_OFFSET,
|
||||||
|
struct.pack('I', type), 4) # rule.action = type
|
||||||
|
|
||||||
|
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
|
||||||
|
'I', PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
|
||||||
|
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
|
||||||
|
|
||||||
|
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
|
||||||
|
'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
|
||||||
|
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
|
||||||
|
|
||||||
|
|
||||||
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
def get_tcp_dstip(self, sock):
|
||||||
|
pfile = self.firewall.pfile
|
||||||
|
|
||||||
|
peer = sock.getpeername()
|
||||||
|
proxy = sock.getsockname()
|
||||||
|
|
||||||
|
argv = (sock.family, socket.IPPROTO_TCP,
|
||||||
|
peer[0], peer[1], proxy[0], proxy[1])
|
||||||
|
pfile.write("QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv)
|
||||||
|
pfile.flush()
|
||||||
|
line = pfile.readline()
|
||||||
|
debug2("QUERY_PF_NAT %d,%d,%s,%d,%s,%d" % argv + ' > ' + line)
|
||||||
|
if line.startswith('QUERY_PF_NAT_SUCCESS '):
|
||||||
|
(ip, port) = line[21:].split(',')
|
||||||
|
return (ip, int(port))
|
||||||
|
|
||||||
|
return sock.getsockname()
|
||||||
|
|
||||||
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
||||||
|
global _pf_started_by_sshuttle
|
||||||
|
tables = []
|
||||||
|
translating_rules = []
|
||||||
|
filtering_rules = []
|
||||||
|
|
||||||
|
if family != socket.AF_INET:
|
||||||
|
raise Exception(
|
||||||
|
'Address family "%s" unsupported by pf method_name'
|
||||||
|
% family_to_string(family))
|
||||||
|
if udp:
|
||||||
|
raise Exception("UDP not supported by pf method_name")
|
||||||
|
|
||||||
|
if subnets:
|
||||||
|
includes = []
|
||||||
|
# If a given subnet is both included and excluded, list the
|
||||||
|
# exclusion first; the table will ignore the second, opposite
|
||||||
|
# definition
|
||||||
|
for f, swidth, sexclude, snet in sorted(
|
||||||
|
subnets, key=lambda s: (s[1], s[2]), reverse=True):
|
||||||
|
includes.append("%s%s/%s" %
|
||||||
|
("!" if sexclude else "", snet, swidth))
|
||||||
|
|
||||||
|
tables.append('table <forward_subnets> {%s}' % ','.join(includes))
|
||||||
|
translating_rules.append(
|
||||||
|
'rdr pass on lo0 proto tcp '
|
||||||
|
'to <forward_subnets> -> 127.0.0.1 port %r' % port)
|
||||||
|
filtering_rules.append(
|
||||||
|
'pass out route-to lo0 inet proto tcp '
|
||||||
|
'to <forward_subnets> keep state')
|
||||||
|
|
||||||
|
if dnsport:
|
||||||
|
tables.append('table <dns_servers> {%s}' % ','.join(
|
||||||
|
[ns[1] for ns in nslist]))
|
||||||
|
translating_rules.append(
|
||||||
|
'rdr pass on lo0 proto udp to '
|
||||||
|
'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
|
||||||
|
filtering_rules.append(
|
||||||
|
'pass out route-to lo0 inet proto udp to '
|
||||||
|
'<dns_servers> port 53 keep state')
|
||||||
|
|
||||||
|
rules = '\n'.join(tables + translating_rules + filtering_rules) \
|
||||||
|
+ '\n'
|
||||||
|
|
||||||
|
pf_status = pfctl('-s all')[0]
|
||||||
|
if '\nrdr-anchor "sshuttle" all\n' not in pf_status:
|
||||||
|
pf_add_anchor_rule(PF_RDR, "sshuttle")
|
||||||
|
if '\nanchor "sshuttle" all\n' not in pf_status:
|
||||||
|
pf_add_anchor_rule(PF_PASS, "sshuttle")
|
||||||
|
|
||||||
|
pfctl('-a sshuttle -f /dev/stdin', rules)
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
o = pfctl('-E')
|
||||||
|
_pf_context['Xtoken'] = \
|
||||||
|
re.search(r'Token : (.+)', o[1]).group(1)
|
||||||
|
elif 'INFO:\nStatus: Disabled' in pf_status:
|
||||||
|
pfctl('-e')
|
||||||
|
_pf_context['started_by_sshuttle'] = True
|
||||||
|
else:
|
||||||
|
pfctl('-a sshuttle -F all')
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
pfctl('-X %s' % _pf_context['Xtoken'])
|
||||||
|
elif _pf_context['started_by_sshuttle']:
|
||||||
|
pfctl('-d')
|
||||||
|
|
||||||
|
def firewall_command(self, line):
|
||||||
|
if line.startswith('QUERY_PF_NAT '):
|
||||||
|
try:
|
||||||
|
dst = pf_query_nat(*(line[13:].split(',')))
|
||||||
|
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
|
||||||
|
except IOError as e:
|
||||||
|
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
|
||||||
|
|
||||||
|
sys.stdout.flush()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
256
sshuttle/methods/tproxy.py
Normal file
256
sshuttle/methods/tproxy.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import struct
|
||||||
|
from sshuttle.helpers import family_to_string
|
||||||
|
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
|
||||||
|
from sshuttle.methods import BaseMethod
|
||||||
|
from sshuttle.helpers import debug1, debug3, Fatal
|
||||||
|
|
||||||
|
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_TRANSPARENT = 19
|
||||||
|
IP_ORIGDSTADDR = 20
|
||||||
|
IP_RECVORIGDSTADDR = IP_ORIGDSTADDR
|
||||||
|
SOL_IPV6 = 41
|
||||||
|
IPV6_ORIGDSTADDR = 74
|
||||||
|
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
|
||||||
|
|
||||||
|
if recvmsg == "python":
|
||||||
|
def recv_udp(listener, bufsize):
|
||||||
|
debug3('Accept UDP python using recvmsg.\n')
|
||||||
|
data, ancdata, msg_flags, srcip = listener.recvmsg(
|
||||||
|
4096, socket.CMSG_SPACE(24))
|
||||||
|
dstip = None
|
||||||
|
family = None
|
||||||
|
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||||
|
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
|
||||||
|
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||||
|
port = socket.htons(port)
|
||||||
|
if family == socket.AF_INET:
|
||||||
|
start = 4
|
||||||
|
length = 4
|
||||||
|
else:
|
||||||
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
|
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||||
|
dstip = (ip, port)
|
||||||
|
break
|
||||||
|
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
|
||||||
|
family, port = struct.unpack('=HH', cmsg_data[0:4])
|
||||||
|
port = socket.htons(port)
|
||||||
|
if family == socket.AF_INET6:
|
||||||
|
start = 8
|
||||||
|
length = 16
|
||||||
|
else:
|
||||||
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
|
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
|
||||||
|
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, flags = listener.recvmsg(
|
||||||
|
(bufsize,), socket.CMSG_SPACE(24))
|
||||||
|
dstip = None
|
||||||
|
family = None
|
||||||
|
for a in adata:
|
||||||
|
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
|
||||||
|
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
||||||
|
port = socket.htons(port)
|
||||||
|
if family == socket.AF_INET:
|
||||||
|
start = 4
|
||||||
|
length = 4
|
||||||
|
else:
|
||||||
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
|
ip = socket.inet_ntop(
|
||||||
|
family, a.cmsg_data[start:start + length])
|
||||||
|
dstip = (ip, port)
|
||||||
|
break
|
||||||
|
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
|
||||||
|
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
|
||||||
|
port = socket.htons(port)
|
||||||
|
if family == socket.AF_INET6:
|
||||||
|
start = 8
|
||||||
|
length = 16
|
||||||
|
else:
|
||||||
|
raise Fatal("Unsupported socket type '%s'" % family)
|
||||||
|
ip = socket.inet_ntop(
|
||||||
|
family, a.cmsg_data[start:start + length])
|
||||||
|
dstip = (ip, port)
|
||||||
|
break
|
||||||
|
return (srcip, dstip, data[0])
|
||||||
|
else:
|
||||||
|
def recv_udp(listener, bufsize):
|
||||||
|
debug3('Accept UDP using recvfrom.\n')
|
||||||
|
data, srcip = listener.recvfrom(bufsize)
|
||||||
|
return (srcip, None, data)
|
||||||
|
|
||||||
|
|
||||||
|
class Method(BaseMethod):
|
||||||
|
|
||||||
|
def get_supported_features(self):
|
||||||
|
result = super(Method, self).get_supported_features()
|
||||||
|
result.ipv6 = True
|
||||||
|
result.udp = 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
|
||||||
|
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
||||||
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
|
sender.bind(srcip)
|
||||||
|
sender.sendto(data, dstip)
|
||||||
|
sender.close()
|
||||||
|
|
||||||
|
def setup_tcp_listener(self, tcp_listener):
|
||||||
|
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
|
|
||||||
|
def setup_udp_listener(self, udp_listener):
|
||||||
|
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
|
||||||
|
if udp_listener.v4 is not None:
|
||||||
|
udp_listener.v4.setsockopt(
|
||||||
|
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
|
||||||
|
if udp_listener.v6 is not None:
|
||||||
|
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
|
||||||
|
|
||||||
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
|
||||||
|
if family not in [socket.AF_INET, socket.AF_INET6]:
|
||||||
|
raise Exception(
|
||||||
|
'Address family "%s" unsupported by tproxy method'
|
||||||
|
% family_to_string(family))
|
||||||
|
|
||||||
|
table = "mangle"
|
||||||
|
|
||||||
|
def _ipt(*args):
|
||||||
|
return ipt(family, table, *args)
|
||||||
|
|
||||||
|
def _ipt_ttl(*args):
|
||||||
|
return ipt_ttl(family, table, *args)
|
||||||
|
|
||||||
|
mark_chain = 'sshuttle-m-%s' % port
|
||||||
|
tproxy_chain = 'sshuttle-t-%s' % port
|
||||||
|
divert_chain = 'sshuttle-d-%s' % port
|
||||||
|
|
||||||
|
# basic cleanup/setup of chains
|
||||||
|
if ipt_chain_exists(family, table, mark_chain):
|
||||||
|
_ipt('-D', 'OUTPUT', '-j', mark_chain)
|
||||||
|
_ipt('-F', mark_chain)
|
||||||
|
_ipt('-X', mark_chain)
|
||||||
|
|
||||||
|
if ipt_chain_exists(family, table, tproxy_chain):
|
||||||
|
_ipt('-D', 'PREROUTING', '-j', tproxy_chain)
|
||||||
|
_ipt('-F', tproxy_chain)
|
||||||
|
_ipt('-X', tproxy_chain)
|
||||||
|
|
||||||
|
if ipt_chain_exists(family, table, divert_chain):
|
||||||
|
_ipt('-F', divert_chain)
|
||||||
|
_ipt('-X', divert_chain)
|
||||||
|
|
||||||
|
if subnets or dnsport:
|
||||||
|
_ipt('-N', mark_chain)
|
||||||
|
_ipt('-F', mark_chain)
|
||||||
|
_ipt('-N', divert_chain)
|
||||||
|
_ipt('-F', divert_chain)
|
||||||
|
_ipt('-N', tproxy_chain)
|
||||||
|
_ipt('-F', tproxy_chain)
|
||||||
|
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
|
||||||
|
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
|
||||||
|
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
|
||||||
|
_ipt('-A', divert_chain, '-j', 'ACCEPT')
|
||||||
|
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||||
|
'-m', 'tcp', '-p', 'tcp')
|
||||||
|
if subnets and udp:
|
||||||
|
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
|
||||||
|
'-m', 'udp', '-p', 'udp')
|
||||||
|
|
||||||
|
if dnsport:
|
||||||
|
for f, ip in [i for i in nslist if i[0] == family]:
|
||||||
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
|
'--dest', '%s/32' % ip,
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53')
|
||||||
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1',
|
||||||
|
'--dest', '%s/32' % ip,
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53',
|
||||||
|
'--on-port', str(dnsport))
|
||||||
|
|
||||||
|
if subnets:
|
||||||
|
for f, swidth, sexclude, snet \
|
||||||
|
in sorted(subnets, key=lambda s: s[1], reverse=True):
|
||||||
|
if sexclude:
|
||||||
|
_ipt('-A', mark_chain, '-j', 'RETURN',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'tcp', '-p', 'tcp')
|
||||||
|
_ipt('-A', tproxy_chain, '-j', 'RETURN',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'tcp', '-p', 'tcp')
|
||||||
|
else:
|
||||||
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'tcp', '-p', 'tcp')
|
||||||
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'tcp', '-p', 'tcp',
|
||||||
|
'--on-port', str(port))
|
||||||
|
|
||||||
|
if sexclude and udp:
|
||||||
|
_ipt('-A', mark_chain, '-j', 'RETURN',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'udp', '-p', 'udp')
|
||||||
|
_ipt('-A', tproxy_chain, '-j', 'RETURN',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'udp', '-p', 'udp')
|
||||||
|
elif udp:
|
||||||
|
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'udp', '-p', 'udp')
|
||||||
|
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1',
|
||||||
|
'--dest', '%s/%s' % (snet, swidth),
|
||||||
|
'-m', 'udp', '-p', 'udp',
|
||||||
|
'--on-port', str(port))
|
||||||
|
|
||||||
|
def check_settings(self, udp, dns):
|
||||||
|
if udp and recvmsg is None:
|
||||||
|
Fatal("tproxy UDP support requires recvmsg function.\n")
|
||||||
|
|
||||||
|
if dns and recvmsg is None:
|
||||||
|
Fatal("tproxy DNS support requires recvmsg function.\n")
|
||||||
|
|
||||||
|
if udp:
|
||||||
|
debug1("tproxy UDP support enabled.\n")
|
||||||
|
|
||||||
|
if dns:
|
||||||
|
debug1("tproxy DNS support enabled.\n")
|
@ -61,6 +61,8 @@ def _remove_negative_k(k):
|
|||||||
|
|
||||||
|
|
||||||
def _tty_width():
|
def _tty_width():
|
||||||
|
if not hasattr(sys.stderr, "fileno"):
|
||||||
|
return _atoi(os.environ.get('WIDTH')) or 70
|
||||||
s = struct.pack("HHHH", 0, 0, 0, 0)
|
s = struct.pack("HHHH", 0, 0, 0, 0)
|
||||||
try:
|
try:
|
||||||
import fcntl
|
import fcntl
|
||||||
@ -184,12 +186,12 @@ class Options:
|
|||||||
try:
|
try:
|
||||||
(flags, extra) = self.optfunc(
|
(flags, extra) = self.optfunc(
|
||||||
args, self._shortopts, self._longopts)
|
args, self._shortopts, self._longopts)
|
||||||
except getopt.GetoptError, e:
|
except getopt.GetoptError as e:
|
||||||
self.fatal(e)
|
self.fatal(e)
|
||||||
|
|
||||||
opt = OptDict()
|
opt = OptDict()
|
||||||
|
|
||||||
for k, v in self._defaults.iteritems():
|
for k, v in self._defaults.items():
|
||||||
k = self._aliases[k]
|
k = self._aliases[k]
|
||||||
opt[k] = v
|
opt[k] = v
|
||||||
|
|
||||||
@ -208,6 +210,6 @@ class Options:
|
|||||||
else:
|
else:
|
||||||
v = _intify(v)
|
v = _intify(v)
|
||||||
opt[k] = v
|
opt[k] = v
|
||||||
for (f1, f2) in self._aliases.iteritems():
|
for (f1, f2) in self._aliases.items():
|
||||||
opt[f1] = opt._opts.get(f2)
|
opt[f1] = opt._opts.get(f2)
|
||||||
return (opt, flags, extra)
|
return (opt, flags, extra)
|
@ -5,18 +5,14 @@ import traceback
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
if not globals().get('skip_imports'):
|
|
||||||
import ssnet
|
|
||||||
import helpers
|
|
||||||
import hostwatch
|
|
||||||
import compat.ssubprocess as ssubprocess
|
|
||||||
from ssnet import Handler, Proxy, Mux, MuxWrapper
|
|
||||||
from helpers import log, debug1, debug2, debug3, Fatal, \
|
|
||||||
resolvconf_random_nameserver
|
|
||||||
|
|
||||||
|
import sshuttle.ssnet as ssnet
|
||||||
if not globals().get('latency_control'):
|
import sshuttle.helpers as helpers
|
||||||
latency_control = None
|
import sshuttle.hostwatch as hostwatch
|
||||||
|
import subprocess as ssubprocess
|
||||||
|
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper
|
||||||
|
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, \
|
||||||
|
resolvconf_random_nameserver
|
||||||
|
|
||||||
|
|
||||||
def _ipmatch(ipstr):
|
def _ipmatch(ipstr):
|
||||||
@ -124,55 +120,62 @@ class Hostwatch:
|
|||||||
class DnsProxy(Handler):
|
class DnsProxy(Handler):
|
||||||
|
|
||||||
def __init__(self, mux, chan, request):
|
def __init__(self, mux, chan, request):
|
||||||
# FIXME! IPv4 specific
|
Handler.__init__(self, [])
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
Handler.__init__(self, [sock])
|
|
||||||
self.timeout = time.time() + 30
|
self.timeout = time.time() + 30
|
||||||
self.mux = mux
|
self.mux = mux
|
||||||
self.chan = chan
|
self.chan = chan
|
||||||
self.tries = 0
|
self.tries = 0
|
||||||
self.peer = None
|
|
||||||
self.request = request
|
self.request = request
|
||||||
self.sock = sock
|
self.peers = {}
|
||||||
# FIXME! IPv4 specific
|
|
||||||
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
|
||||||
self.try_send()
|
self.try_send()
|
||||||
|
|
||||||
def try_send(self):
|
def try_send(self):
|
||||||
if self.tries >= 3:
|
if self.tries >= 3:
|
||||||
return
|
return
|
||||||
self.tries += 1
|
self.tries += 1
|
||||||
# FIXME! Support IPv6 nameservers
|
|
||||||
self.peer = resolvconf_random_nameserver()[1]
|
family, peer = resolvconf_random_nameserver()
|
||||||
self.sock.connect((self.peer, 53))
|
|
||||||
debug2('DNS: sending to %r\n' % self.peer)
|
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
|
sock.connect((peer, 53))
|
||||||
|
|
||||||
|
self.peers[sock] = peer
|
||||||
|
|
||||||
|
debug2('DNS: sending to %r (try %d)\n' % (peer, self.tries))
|
||||||
try:
|
try:
|
||||||
self.sock.send(self.request)
|
sock.send(self.request)
|
||||||
except socket.error, e:
|
self.socks.append(sock)
|
||||||
|
except socket.error as e:
|
||||||
if e.args[0] in ssnet.NET_ERRS:
|
if e.args[0] in ssnet.NET_ERRS:
|
||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
# and sometimes by send(). We have to catch both.
|
# and sometimes by send(). We have to catch both.
|
||||||
debug2('DNS send to %r: %s\n' % (self.peer, e))
|
debug2('DNS send to %r: %s\n' % (peer, e))
|
||||||
self.try_send()
|
self.try_send()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
log('DNS send to %r: %s\n' % (self.peer, e))
|
log('DNS send to %r: %s\n' % (peer, e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self):
|
def callback(self, sock):
|
||||||
|
peer = self.peers[sock]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = self.sock.recv(4096)
|
data = sock.recv(4096)
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
|
self.socks.remove(sock)
|
||||||
|
del self.peers[sock]
|
||||||
|
|
||||||
if e.args[0] in ssnet.NET_ERRS:
|
if e.args[0] in ssnet.NET_ERRS:
|
||||||
# might have been spurious; try again.
|
# might have been spurious; try again.
|
||||||
# Note: these errors sometimes are reported by recv(),
|
# Note: these errors sometimes are reported by recv(),
|
||||||
# and sometimes by send(). We have to catch both.
|
# and sometimes by send(). We have to catch both.
|
||||||
debug2('DNS recv from %r: %s\n' % (self.peer, e))
|
debug2('DNS recv from %r: %s\n' % (peer, e))
|
||||||
self.try_send()
|
self.try_send()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
log('DNS recv from %r: %s\n' % (self.peer, e))
|
log('DNS recv from %r: %s\n' % (peer, e))
|
||||||
return
|
return
|
||||||
debug2('DNS response: %d bytes\n' % len(data))
|
debug2('DNS response: %d bytes\n' % len(data))
|
||||||
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
||||||
@ -195,14 +198,14 @@ class UdpProxy(Handler):
|
|||||||
debug2('UDP: sending to %r port %d\n' % dstip)
|
debug2('UDP: sending to %r port %d\n' % dstip)
|
||||||
try:
|
try:
|
||||||
self.sock.sendto(data, dstip)
|
self.sock.sendto(data, dstip)
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
|
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
|
||||||
return
|
return
|
||||||
|
|
||||||
def callback(self):
|
def callback(self, sock):
|
||||||
try:
|
try:
|
||||||
data, peer = self.sock.recvfrom(4096)
|
data, peer = sock.recvfrom(4096)
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
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))
|
||||||
@ -210,12 +213,11 @@ class UdpProxy(Handler):
|
|||||||
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(latency_control):
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = ' s: '
|
helpers.logprefix = ' s: '
|
||||||
else:
|
else:
|
||||||
helpers.logprefix = 'server: '
|
helpers.logprefix = 'server: '
|
||||||
assert latency_control is not None
|
|
||||||
debug1('latency control setting = %r\n' % latency_control)
|
debug1('latency control setting = %r\n' % latency_control)
|
||||||
|
|
||||||
routes = list(list_routes())
|
routes = list(list_routes())
|
||||||
@ -241,7 +243,7 @@ def main():
|
|||||||
hw = Hostwatch()
|
hw = Hostwatch()
|
||||||
hw.leftover = ''
|
hw.leftover = ''
|
||||||
|
|
||||||
def hostwatch_ready():
|
def hostwatch_ready(sock):
|
||||||
assert(hw.pid)
|
assert(hw.pid)
|
||||||
content = hw.sock.recv(4096)
|
content = hw.sock.recv(4096)
|
||||||
if content:
|
if content:
|
||||||
@ -319,17 +321,16 @@ def main():
|
|||||||
ssnet.runonce(handlers, mux)
|
ssnet.runonce(handlers, mux)
|
||||||
if latency_control:
|
if latency_control:
|
||||||
mux.check_fullness()
|
mux.check_fullness()
|
||||||
mux.callback()
|
|
||||||
|
|
||||||
if dnshandlers:
|
if dnshandlers:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for channel, h in dnshandlers.items():
|
for channel, h in list(dnshandlers.items()):
|
||||||
if h.timeout < now or not h.ok:
|
if h.timeout < now or not h.ok:
|
||||||
debug3('expiring dnsreqs channel=%d\n' % channel)
|
debug3('expiring dnsreqs channel=%d\n' % channel)
|
||||||
del dnshandlers[channel]
|
del dnshandlers[channel]
|
||||||
h.ok = False
|
h.ok = False
|
||||||
if udphandlers:
|
if udphandlers:
|
||||||
for channel, h in udphandlers.items():
|
for channel, h in list(udphandlers.items()):
|
||||||
if not h.ok:
|
if not h.ok:
|
||||||
debug3('expiring UDP channel=%d\n' % channel)
|
debug3('expiring UDP channel=%d\n' % channel)
|
||||||
del udphandlers[channel]
|
del udphandlers[channel]
|
@ -3,28 +3,53 @@ import os
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import zlib
|
import zlib
|
||||||
import compat.ssubprocess as ssubprocess
|
import imp
|
||||||
import helpers
|
import subprocess as ssubprocess
|
||||||
from helpers import debug2
|
import sshuttle.helpers as helpers
|
||||||
|
from sshuttle.helpers import debug2
|
||||||
|
|
||||||
|
|
||||||
def readfile(name):
|
def readfile(name):
|
||||||
basedir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
tokens = name.split(".")
|
||||||
path = [basedir] + sys.path
|
f = None
|
||||||
for d in path:
|
|
||||||
fullname = os.path.join(d, name)
|
token = tokens[0]
|
||||||
if os.path.exists(fullname):
|
token_name = [token]
|
||||||
return open(fullname, 'rb').read()
|
token_str = ".".join(token_name)
|
||||||
raise Exception("can't find file %r in any of %r" % (name, path))
|
|
||||||
|
try:
|
||||||
|
f, pathname, description = imp.find_module(token_str)
|
||||||
|
|
||||||
|
for token in tokens[1:]:
|
||||||
|
module = imp.load_module(token_str, f, pathname, description)
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
token_name.append(token)
|
||||||
|
token_str = ".".join(token_name)
|
||||||
|
|
||||||
|
f, pathname, description = imp.find_module(
|
||||||
|
token, module.__path__)
|
||||||
|
|
||||||
|
if f is not None:
|
||||||
|
contents = f.read()
|
||||||
|
else:
|
||||||
|
contents = ""
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return contents.encode("UTF8")
|
||||||
|
|
||||||
|
|
||||||
def empackage(z, filename, data=None):
|
def empackage(z, name, data=None):
|
||||||
(path, basename) = os.path.split(filename)
|
|
||||||
if not data:
|
if not data:
|
||||||
data = readfile(filename)
|
data = readfile(name)
|
||||||
content = z.compress(data)
|
content = z.compress(data)
|
||||||
content += z.flush(zlib.Z_SYNC_FLUSH)
|
content += z.flush(zlib.Z_SYNC_FLUSH)
|
||||||
return '%s\n%d\n%s' % (basename, len(content), content)
|
|
||||||
|
return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content)
|
||||||
|
|
||||||
|
|
||||||
def connect(ssh_cmd, rhostport, python, stderr, options):
|
def connect(ssh_cmd, rhostport, python, stderr, options):
|
||||||
@ -52,19 +77,19 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
|
|||||||
rhost = None
|
rhost = None
|
||||||
|
|
||||||
z = zlib.compressobj(1)
|
z = zlib.compressobj(1)
|
||||||
content = readfile('assembler.py')
|
content = readfile('sshuttle.assembler')
|
||||||
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in options.items())
|
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
|
||||||
content2 = (empackage(z, 'cmdline_options.py', optdata) +
|
optdata = optdata.encode("UTF8")
|
||||||
empackage(z, 'helpers.py') +
|
content2 = (empackage(z, 'sshuttle') +
|
||||||
empackage(z, 'compat/ssubprocess.py') +
|
empackage(z, 'sshuttle.cmdline_options', optdata) +
|
||||||
empackage(z, 'ssnet.py') +
|
empackage(z, 'sshuttle.helpers') +
|
||||||
empackage(z, 'hostwatch.py') +
|
empackage(z, 'sshuttle.ssnet') +
|
||||||
empackage(z, 'server.py') +
|
empackage(z, 'sshuttle.hostwatch') +
|
||||||
"\n")
|
empackage(z, 'sshuttle.server') +
|
||||||
|
b"\n")
|
||||||
|
|
||||||
pyscript = r"""
|
pyscript = r"""
|
||||||
import sys;
|
import sys;
|
||||||
skip_imports=1;
|
|
||||||
verbosity=%d;
|
verbosity=%d;
|
||||||
exec compile(sys.stdin.read(%d), "assembler.py", "exec")
|
exec compile(sys.stdin.read(%d), "assembler.py", "exec")
|
||||||
""" % (helpers.verbose or 0, len(content))
|
""" % (helpers.verbose or 0, len(content))
|
@ -147,11 +147,6 @@ entire subnet to the VPN.
|
|||||||
*pidfilename*. The default is `sshuttle.pid` in the
|
*pidfilename*. The default is `sshuttle.pid` in the
|
||||||
current directory.
|
current directory.
|
||||||
|
|
||||||
--server
|
|
||||||
: (internal use only) run the sshuttle server on
|
|
||||||
stdin/stdout. This is what the client runs on
|
|
||||||
the remote end.
|
|
||||||
|
|
||||||
--firewall
|
--firewall
|
||||||
: (internal use only) run the firewall manager. This is
|
: (internal use only) run the firewall manager. This is
|
||||||
the only part of sshuttle that must run as root. If
|
the only part of sshuttle that must run as root. If
|
@ -3,8 +3,7 @@ import socket
|
|||||||
import errno
|
import errno
|
||||||
import select
|
import select
|
||||||
import os
|
import os
|
||||||
if not globals().get('skip_imports'):
|
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal
|
||||||
from helpers import log, debug1, debug2, debug3, Fatal
|
|
||||||
|
|
||||||
MAX_CHANNEL = 65535
|
MAX_CHANNEL = 65535
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
|
|||||||
|
|
||||||
|
|
||||||
def _add(l, elem):
|
def _add(l, elem):
|
||||||
if not elem in l:
|
if elem not in l:
|
||||||
l.append(elem)
|
l.append(elem)
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ def _fds(l):
|
|||||||
def _nb_clean(func, *args):
|
def _nb_clean(func, *args):
|
||||||
try:
|
try:
|
||||||
return func(*args)
|
return func(*args)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
@ -89,7 +88,7 @@ def _try_peername(sock):
|
|||||||
pn = sock.getpeername()
|
pn = sock.getpeername()
|
||||||
if pn:
|
if pn:
|
||||||
return '%s:%s' % (pn[0], pn[1])
|
return '%s:%s' % (pn[0], pn[1])
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
|
||||||
raise
|
raise
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
@ -145,7 +144,7 @@ class SockWrapper:
|
|||||||
self.rsock.connect(self.connect_to)
|
self.rsock.connect(self.connect_to)
|
||||||
# connected successfully (Linux)
|
# connected successfully (Linux)
|
||||||
self.connect_to = None
|
self.connect_to = None
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
debug3('%r: connect result: %s\n' % (self, e))
|
debug3('%r: connect result: %s\n' % (self, e))
|
||||||
if e.args[0] == errno.EINVAL:
|
if e.args[0] == errno.EINVAL:
|
||||||
# this is what happens when you call connect() on a socket
|
# this is what happens when you call connect() on a socket
|
||||||
@ -192,7 +191,7 @@ class SockWrapper:
|
|||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
try:
|
try:
|
||||||
self.wsock.shutdown(SHUT_WR)
|
self.wsock.shutdown(SHUT_WR)
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
self.seterr('nowrite: %s' % e)
|
self.seterr('nowrite: %s' % e)
|
||||||
|
|
||||||
def too_full(self):
|
def too_full(self):
|
||||||
@ -204,7 +203,7 @@ class SockWrapper:
|
|||||||
self.wsock.setblocking(False)
|
self.wsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
return _nb_clean(os.write, self.wsock.fileno(), buf)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
if e.errno == errno.EPIPE:
|
if e.errno == errno.EPIPE:
|
||||||
debug1('%r: uwrite: got EPIPE\n' % self)
|
debug1('%r: uwrite: got EPIPE\n' % self)
|
||||||
self.nowrite()
|
self.nowrite()
|
||||||
@ -226,9 +225,9 @@ class SockWrapper:
|
|||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
return _nb_clean(os.read, self.rsock.fileno(), 65536)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
self.seterr('uread: %s' % e)
|
self.seterr('uread: %s' % e)
|
||||||
return '' # unexpected error... we'll call it EOF
|
return b'' # unexpected error... we'll call it EOF
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
if self.buf:
|
if self.buf:
|
||||||
@ -236,7 +235,7 @@ class SockWrapper:
|
|||||||
rb = self.uread()
|
rb = self.uread()
|
||||||
if rb:
|
if rb:
|
||||||
self.buf.append(rb)
|
self.buf.append(rb)
|
||||||
if rb == '': # empty string means EOF; None means temporarily empty
|
if rb == b'': # empty string means EOF; None means temporarily empty
|
||||||
self.noread()
|
self.noread()
|
||||||
|
|
||||||
def copy_to(self, outwrap):
|
def copy_to(self, outwrap):
|
||||||
@ -261,7 +260,7 @@ class Handler:
|
|||||||
for i in self.socks:
|
for i in self.socks:
|
||||||
_add(r, i)
|
_add(r, i)
|
||||||
|
|
||||||
def callback(self):
|
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, w, x) = select.select(self.socks, [], [], 0)
|
||||||
for s in r:
|
for s in r:
|
||||||
@ -302,7 +301,7 @@ class Proxy(Handler):
|
|||||||
elif not self.wrap2.shut_read:
|
elif not self.wrap2.shut_read:
|
||||||
_add(r, self.wrap2.rsock)
|
_add(r, self.wrap2.rsock)
|
||||||
|
|
||||||
def callback(self):
|
def callback(self, sock):
|
||||||
self.wrap1.try_connect()
|
self.wrap1.try_connect()
|
||||||
self.wrap2.try_connect()
|
self.wrap2.try_connect()
|
||||||
self.wrap1.fill()
|
self.wrap1.fill()
|
||||||
@ -334,15 +333,15 @@ class Mux(Handler):
|
|||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.chani = 0
|
self.chani = 0
|
||||||
self.want = 0
|
self.want = 0
|
||||||
self.inbuf = ''
|
self.inbuf = b''
|
||||||
self.outbuf = []
|
self.outbuf = []
|
||||||
self.fullness = 0
|
self.fullness = 0
|
||||||
self.too_full = False
|
self.too_full = False
|
||||||
self.send(0, CMD_PING, 'chicken')
|
self.send(0, CMD_PING, b'chicken')
|
||||||
|
|
||||||
def next_channel(self):
|
def next_channel(self):
|
||||||
# channel 0 is special, so we never allocate it
|
# channel 0 is special, so we never allocate it
|
||||||
for timeout in xrange(1024):
|
for timeout 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
|
||||||
@ -358,18 +357,18 @@ class Mux(Handler):
|
|||||||
def check_fullness(self):
|
def check_fullness(self):
|
||||||
if self.fullness > 32768:
|
if self.fullness > 32768:
|
||||||
if not self.too_full:
|
if not self.too_full:
|
||||||
self.send(0, CMD_PING, 'rttest')
|
self.send(0, CMD_PING, b'rttest')
|
||||||
self.too_full = True
|
self.too_full = True
|
||||||
#ob = []
|
# ob = []
|
||||||
# for b in self.outbuf:
|
# for b in self.outbuf:
|
||||||
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
|
# (s1,s2,c) = struct.unpack('!ccH', b[:4])
|
||||||
# ob.append(c)
|
# ob.append(c)
|
||||||
#log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
# log('outbuf: %d %r\n' % (self.amount_queued(), ob))
|
||||||
|
|
||||||
def send(self, channel, cmd, data):
|
def send(self, channel, cmd, data):
|
||||||
data = str(data)
|
assert isinstance(data, bytes)
|
||||||
assert(len(data) <= 65535)
|
assert len(data) <= 65535
|
||||||
p = struct.pack('!ccHHH', 'S', '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)),
|
||||||
@ -436,10 +435,10 @@ class Mux(Handler):
|
|||||||
self.rsock.setblocking(False)
|
self.rsock.setblocking(False)
|
||||||
try:
|
try:
|
||||||
b = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
b = _nb_clean(os.read, self.rsock.fileno(), 32768)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
raise Fatal('other end: %r' % e)
|
raise Fatal('other end: %r' % e)
|
||||||
#log('<<< %r\n' % b)
|
# log('<<< %r\n' % b)
|
||||||
if b == '': # EOF
|
if b == b'': # EOF
|
||||||
self.ok = False
|
self.ok = False
|
||||||
if b:
|
if b:
|
||||||
self.inbuf += b
|
self.inbuf += b
|
||||||
@ -452,8 +451,8 @@ class Mux(Handler):
|
|||||||
if len(self.inbuf) >= (self.want or HDR_LEN):
|
if len(self.inbuf) >= (self.want or HDR_LEN):
|
||||||
(s1, s2, channel, cmd, datalen) = \
|
(s1, s2, channel, cmd, datalen) = \
|
||||||
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
|
||||||
assert(s1 == 'S')
|
assert(s1 == b'S')
|
||||||
assert(s2 == 'S')
|
assert(s2 == b'S')
|
||||||
self.want = datalen + HDR_LEN
|
self.want = datalen + HDR_LEN
|
||||||
if self.want and len(self.inbuf) >= self.want:
|
if self.want and len(self.inbuf) >= self.want:
|
||||||
data = self.inbuf[HDR_LEN:self.want]
|
data = self.inbuf[HDR_LEN:self.want]
|
||||||
@ -468,7 +467,7 @@ class Mux(Handler):
|
|||||||
if self.outbuf:
|
if self.outbuf:
|
||||||
_add(w, self.wsock)
|
_add(w, self.wsock)
|
||||||
|
|
||||||
def callback(self):
|
def callback(self, sock):
|
||||||
(r, w, x) = select.select([self.rsock], [self.wsock], [], 0)
|
(r, w, x) = select.select([self.rsock], [self.wsock], [], 0)
|
||||||
if self.rsock in r:
|
if self.rsock in r:
|
||||||
self.handle()
|
self.handle()
|
||||||
@ -495,18 +494,21 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def noread(self):
|
def noread(self):
|
||||||
if not self.shut_read:
|
if not self.shut_read:
|
||||||
|
debug2('%r: done reading\n' % self)
|
||||||
self.shut_read = True
|
self.shut_read = True
|
||||||
self.mux.send(self.channel, CMD_TCP_STOP_SENDING, '')
|
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:
|
if not self.shut_write:
|
||||||
|
debug2('%r: done writing\n' % self)
|
||||||
self.shut_write = True
|
self.shut_write = True
|
||||||
self.mux.send(self.channel, CMD_TCP_EOF, '')
|
self.mux.send(self.channel, CMD_TCP_EOF, b'')
|
||||||
self.maybe_close()
|
self.maybe_close()
|
||||||
|
|
||||||
def maybe_close(self):
|
def maybe_close(self):
|
||||||
if self.shut_read and self.shut_write:
|
if self.shut_read and self.shut_write:
|
||||||
|
debug2('%r: closing connection\n' % self)
|
||||||
# remove the mux's reference to us. The python garbage collector
|
# remove the mux's reference to us. The python garbage collector
|
||||||
# will then be able to reap our object.
|
# will then be able to reap our object.
|
||||||
self.mux.channels[self.channel] = None
|
self.mux.channels[self.channel] = None
|
||||||
@ -524,7 +526,7 @@ class MuxWrapper(SockWrapper):
|
|||||||
|
|
||||||
def uread(self):
|
def uread(self):
|
||||||
if self.shut_read:
|
if self.shut_read:
|
||||||
return '' # EOF
|
return b'' # EOF
|
||||||
else:
|
else:
|
||||||
return None # no data available right now
|
return None # no data available right now
|
||||||
|
|
||||||
@ -553,7 +555,7 @@ def runonce(handlers, mux):
|
|||||||
r = []
|
r = []
|
||||||
w = []
|
w = []
|
||||||
x = []
|
x = []
|
||||||
to_remove = filter(lambda s: not s.ok, handlers)
|
to_remove = [s for s in handlers if not s.ok]
|
||||||
for h in to_remove:
|
for h in to_remove:
|
||||||
handlers.remove(h)
|
handlers.remove(h)
|
||||||
|
|
||||||
@ -570,8 +572,8 @@ def runonce(handlers, mux):
|
|||||||
for h in handlers:
|
for h in handlers:
|
||||||
for s in h.socks:
|
for s in h.socks:
|
||||||
if s in ready:
|
if s in ready:
|
||||||
h.callback()
|
h.callback(s)
|
||||||
did[s] = 1
|
did[s] = 1
|
||||||
for s in ready:
|
for s in ready:
|
||||||
if not s in did:
|
if s not in did:
|
||||||
raise Fatal('socket %r was not used by any handler' % s)
|
raise Fatal('socket %r was not used by any handler' % s)
|
@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from compat import ssubprocess
|
import subprocess as ssubprocess
|
||||||
|
|
||||||
|
|
||||||
_p = None
|
_p = None
|
@ -25,10 +25,10 @@ while 1:
|
|||||||
count += 1
|
count += 1
|
||||||
if count >= 16384:
|
if count >= 16384:
|
||||||
count = 1
|
count = 1
|
||||||
print 'cli CREATING %d' % count
|
print('cli CREATING %d' % count)
|
||||||
b = struct.pack('I', count) + 'x' * count
|
b = struct.pack('I', count) + 'x' * count
|
||||||
remain[c] = count
|
remain[c] = count
|
||||||
print 'cli >> %r' % len(b)
|
print('cli >> %r' % len(b))
|
||||||
c.send(b)
|
c.send(b)
|
||||||
c.shutdown(socket.SHUT_WR)
|
c.shutdown(socket.SHUT_WR)
|
||||||
clients.append(c)
|
clients.append(c)
|
||||||
@ -36,7 +36,7 @@ while 1:
|
|||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
else:
|
else:
|
||||||
r = [listener] + servers + clients
|
r = [listener] + servers + clients
|
||||||
print 'select(%d)' % len(r)
|
print('select(%d)' % len(r))
|
||||||
r, w, x = select.select(r, [], [], 5)
|
r, w, x = select.select(r, [], [], 5)
|
||||||
assert(r)
|
assert(r)
|
||||||
for i in r:
|
for i in r:
|
||||||
@ -45,8 +45,8 @@ while 1:
|
|||||||
servers.append(s)
|
servers.append(s)
|
||||||
elif i in servers:
|
elif i in servers:
|
||||||
b = i.recv(4096)
|
b = i.recv(4096)
|
||||||
print 'srv << %r' % len(b)
|
print('srv << %r' % len(b))
|
||||||
if not i in remain:
|
if i not in remain:
|
||||||
assert(len(b) >= 4)
|
assert(len(b) >= 4)
|
||||||
want = struct.unpack('I', b[:4])[0]
|
want = struct.unpack('I', b[:4])[0]
|
||||||
b = b[4:]
|
b = b[4:]
|
||||||
@ -54,34 +54,34 @@ while 1:
|
|||||||
else:
|
else:
|
||||||
want = remain[i]
|
want = remain[i]
|
||||||
if want < len(b):
|
if want < len(b):
|
||||||
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
|
print('weird wanted %d bytes, got %d: %r' % (want, len(b), b))
|
||||||
assert(want >= len(b))
|
assert(want >= len(b))
|
||||||
want -= len(b)
|
want -= len(b)
|
||||||
remain[i] = want
|
remain[i] = want
|
||||||
if not b: # EOF
|
if not b: # EOF
|
||||||
if want:
|
if want:
|
||||||
print 'weird: eof but wanted %d more' % want
|
print('weird: eof but wanted %d more' % want)
|
||||||
assert(want == 0)
|
assert(want == 0)
|
||||||
i.close()
|
i.close()
|
||||||
servers.remove(i)
|
servers.remove(i)
|
||||||
del remain[i]
|
del remain[i]
|
||||||
else:
|
else:
|
||||||
print 'srv >> %r' % len(b)
|
print('srv >> %r' % len(b))
|
||||||
i.send('y' * len(b))
|
i.send('y' * len(b))
|
||||||
if not want:
|
if not want:
|
||||||
i.shutdown(socket.SHUT_WR)
|
i.shutdown(socket.SHUT_WR)
|
||||||
elif i in clients:
|
elif i in clients:
|
||||||
b = i.recv(4096)
|
b = i.recv(4096)
|
||||||
print 'cli << %r' % len(b)
|
print('cli << %r' % len(b))
|
||||||
want = remain[i]
|
want = remain[i]
|
||||||
if want < len(b):
|
if want < len(b):
|
||||||
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
|
print('weird wanted %d bytes, got %d: %r' % (want, len(b), b))
|
||||||
assert(want >= len(b))
|
assert(want >= len(b))
|
||||||
want -= len(b)
|
want -= len(b)
|
||||||
remain[i] = want
|
remain[i] = want
|
||||||
if not b: # EOF
|
if not b: # EOF
|
||||||
if want:
|
if want:
|
||||||
print 'weird: eof but wanted %d more' % want
|
print('weird: eof but wanted %d more' % want)
|
||||||
assert(want == 0)
|
assert(want == 0)
|
||||||
i.close()
|
i.close()
|
||||||
clients.remove(i)
|
clients.remove(i)
|
109
sshuttle/tests/test_firewall.py
Normal file
109
sshuttle/tests/test_firewall.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from mock import Mock, patch, call
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import filecmp
|
||||||
|
|
||||||
|
import sshuttle.firewall
|
||||||
|
|
||||||
|
|
||||||
|
def setup_daemon():
|
||||||
|
stdin = io.StringIO(u"""ROUTES
|
||||||
|
2,24,0,1.2.3.0
|
||||||
|
2,32,1,1.2.3.66
|
||||||
|
10,64,0,2404:6800:4004:80c::
|
||||||
|
10,128,1,2404:6800:4004:80c::101f
|
||||||
|
NSLIST
|
||||||
|
2,1.2.3.33
|
||||||
|
10,2404:6800:4004:80c::33
|
||||||
|
PORTS 1024,1025,1026,1027
|
||||||
|
GO 1
|
||||||
|
""")
|
||||||
|
stdout = Mock()
|
||||||
|
return stdin, stdout
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.firewall.HOSTSFILE', new='tmp/hosts')
|
||||||
|
@patch('sshuttle.firewall.hostmap', new={
|
||||||
|
'myhost': '1.2.3.4',
|
||||||
|
'myotherhost': '1.2.3.5',
|
||||||
|
})
|
||||||
|
def test_rewrite_etc_hosts():
|
||||||
|
if not os.path.isdir("tmp"):
|
||||||
|
os.mkdir("tmp")
|
||||||
|
|
||||||
|
with open("tmp/hosts.orig", "w") as f:
|
||||||
|
f.write("1.2.3.3 existing\n")
|
||||||
|
|
||||||
|
shutil.copyfile("tmp/hosts.orig", "tmp/hosts")
|
||||||
|
|
||||||
|
sshuttle.firewall.rewrite_etc_hosts(10)
|
||||||
|
with open("tmp/hosts") as f:
|
||||||
|
line = f.readline()
|
||||||
|
s = line.split()
|
||||||
|
assert s == ['1.2.3.3', 'existing']
|
||||||
|
|
||||||
|
line = f.readline()
|
||||||
|
s = line.split()
|
||||||
|
assert s == ['1.2.3.4', 'myhost',
|
||||||
|
'#', 'sshuttle-firewall-10', 'AUTOCREATED']
|
||||||
|
|
||||||
|
line = f.readline()
|
||||||
|
s = line.split()
|
||||||
|
assert s == ['1.2.3.5', 'myotherhost',
|
||||||
|
'#', 'sshuttle-firewall-10', 'AUTOCREATED']
|
||||||
|
|
||||||
|
line = f.readline()
|
||||||
|
assert line == ""
|
||||||
|
|
||||||
|
sshuttle.firewall.restore_etc_hosts(10)
|
||||||
|
assert filecmp.cmp("tmp/hosts.orig", "tmp/hosts", shallow=False) is True
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.firewall.HOSTSFILE', new='tmp/hosts')
|
||||||
|
@patch('sshuttle.firewall.setup_daemon')
|
||||||
|
@patch('sshuttle.firewall.get_method')
|
||||||
|
def test_main(mock_get_method, mock_setup_daemon):
|
||||||
|
stdin, stdout = setup_daemon()
|
||||||
|
mock_setup_daemon.return_value = stdin, stdout
|
||||||
|
|
||||||
|
if not os.path.isdir("tmp"):
|
||||||
|
os.mkdir("tmp")
|
||||||
|
|
||||||
|
sshuttle.firewall.main("test", False)
|
||||||
|
|
||||||
|
with open("tmp/hosts") as f:
|
||||||
|
line = f.readline()
|
||||||
|
s = line.split()
|
||||||
|
assert s == ['1.2.3.3', 'existing']
|
||||||
|
|
||||||
|
line = f.readline()
|
||||||
|
assert line == ""
|
||||||
|
|
||||||
|
stdout.mock_calls == [
|
||||||
|
call.write('READY test\n'),
|
||||||
|
call.flush(),
|
||||||
|
call.write('STARTED\n'),
|
||||||
|
call.flush()
|
||||||
|
]
|
||||||
|
mock_setup_daemon.mock_calls == [call()]
|
||||||
|
mock_get_method.mock_calls == [
|
||||||
|
call('test'),
|
||||||
|
call().setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(10, u'2404:6800:4004:80c::33')],
|
||||||
|
10,
|
||||||
|
[(10, 64, False, u'2404:6800:4004:80c::'),
|
||||||
|
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
||||||
|
True),
|
||||||
|
call().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),
|
||||||
|
call().setup_firewall()(),
|
||||||
|
call().setup_firewall(1024, 0, [], 10, [], True),
|
||||||
|
call().setup_firewall(1025, 0, [], 2, [], True),
|
||||||
|
]
|
171
sshuttle/tests/test_helpers.py
Normal file
171
sshuttle/tests/test_helpers.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
from mock import patch, call
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import sshuttle.helpers
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.logprefix', new='prefix: ')
|
||||||
|
@patch('sshuttle.helpers.sys.stdout')
|
||||||
|
@patch('sshuttle.helpers.sys.stderr')
|
||||||
|
def test_log(mock_stderr, mock_stdout):
|
||||||
|
sshuttle.helpers.log("message")
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
assert mock_stderr.mock_calls == [
|
||||||
|
call.write('prefix: message'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.logprefix', new='prefix: ')
|
||||||
|
@patch('sshuttle.helpers.verbose', new=1)
|
||||||
|
@patch('sshuttle.helpers.sys.stdout')
|
||||||
|
@patch('sshuttle.helpers.sys.stderr')
|
||||||
|
def test_debug1(mock_stderr, mock_stdout):
|
||||||
|
sshuttle.helpers.debug1("message")
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
assert mock_stderr.mock_calls == [
|
||||||
|
call.write('prefix: message'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.logprefix', new='prefix: ')
|
||||||
|
@patch('sshuttle.helpers.verbose', new=0)
|
||||||
|
@patch('sshuttle.helpers.sys.stdout')
|
||||||
|
@patch('sshuttle.helpers.sys.stderr')
|
||||||
|
def test_debug1_nop(mock_stderr, mock_stdout):
|
||||||
|
sshuttle.helpers.debug1("message")
|
||||||
|
assert mock_stdout.mock_calls == []
|
||||||
|
assert mock_stderr.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.logprefix', new='prefix: ')
|
||||||
|
@patch('sshuttle.helpers.verbose', new=2)
|
||||||
|
@patch('sshuttle.helpers.sys.stdout')
|
||||||
|
@patch('sshuttle.helpers.sys.stderr')
|
||||||
|
def test_debug2(mock_stderr, mock_stdout):
|
||||||
|
sshuttle.helpers.debug2("message")
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
assert mock_stderr.mock_calls == [
|
||||||
|
call.write('prefix: message'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.logprefix', new='prefix: ')
|
||||||
|
@patch('sshuttle.helpers.verbose', new=1)
|
||||||
|
@patch('sshuttle.helpers.sys.stdout')
|
||||||
|
@patch('sshuttle.helpers.sys.stderr')
|
||||||
|
def test_debug2_nop(mock_stderr, mock_stdout):
|
||||||
|
sshuttle.helpers.debug2("message")
|
||||||
|
assert mock_stdout.mock_calls == []
|
||||||
|
assert mock_stderr.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.logprefix', new='prefix: ')
|
||||||
|
@patch('sshuttle.helpers.verbose', new=3)
|
||||||
|
@patch('sshuttle.helpers.sys.stdout')
|
||||||
|
@patch('sshuttle.helpers.sys.stderr')
|
||||||
|
def test_debug3(mock_stderr, mock_stdout):
|
||||||
|
sshuttle.helpers.debug3("message")
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
assert mock_stderr.mock_calls == [
|
||||||
|
call.write('prefix: message'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.logprefix', new='prefix: ')
|
||||||
|
@patch('sshuttle.helpers.verbose', new=2)
|
||||||
|
@patch('sshuttle.helpers.sys.stdout')
|
||||||
|
@patch('sshuttle.helpers.sys.stderr')
|
||||||
|
def test_debug3_nop(mock_stderr, mock_stdout):
|
||||||
|
sshuttle.helpers.debug3("message")
|
||||||
|
assert mock_stdout.mock_calls == []
|
||||||
|
assert mock_stderr.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.open', create=True)
|
||||||
|
def test_resolvconf_nameservers(mock_open):
|
||||||
|
mock_open.return_value = io.StringIO(u"""
|
||||||
|
# Generated by NetworkManager
|
||||||
|
search pri
|
||||||
|
nameserver 192.168.1.1
|
||||||
|
nameserver 192.168.2.1
|
||||||
|
nameserver 192.168.3.1
|
||||||
|
nameserver 192.168.4.1
|
||||||
|
nameserver 2404:6800:4004:80c::1
|
||||||
|
nameserver 2404:6800:4004:80c::2
|
||||||
|
nameserver 2404:6800:4004:80c::3
|
||||||
|
nameserver 2404:6800:4004:80c::4
|
||||||
|
""")
|
||||||
|
|
||||||
|
ns = sshuttle.helpers.resolvconf_nameservers()
|
||||||
|
assert ns == [
|
||||||
|
(2, u'192.168.1.1'), (2, u'192.168.2.1'),
|
||||||
|
(2, u'192.168.3.1'), (2, u'192.168.4.1'),
|
||||||
|
(10, u'2404:6800:4004:80c::1'), (10, u'2404:6800:4004:80c::2'),
|
||||||
|
(10, u'2404:6800:4004:80c::3'), (10, u'2404:6800:4004:80c::4')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.helpers.open', create=True)
|
||||||
|
def test_resolvconf_random_nameserver(mock_open):
|
||||||
|
mock_open.return_value = io.StringIO(u"""
|
||||||
|
# Generated by NetworkManager
|
||||||
|
search pri
|
||||||
|
nameserver 192.168.1.1
|
||||||
|
nameserver 192.168.2.1
|
||||||
|
nameserver 192.168.3.1
|
||||||
|
nameserver 192.168.4.1
|
||||||
|
nameserver 2404:6800:4004:80c::1
|
||||||
|
nameserver 2404:6800:4004:80c::2
|
||||||
|
nameserver 2404:6800:4004:80c::3
|
||||||
|
nameserver 2404:6800:4004:80c::4
|
||||||
|
""")
|
||||||
|
ns = sshuttle.helpers.resolvconf_random_nameserver()
|
||||||
|
assert ns in [
|
||||||
|
(2, u'192.168.1.1'), (2, u'192.168.2.1'),
|
||||||
|
(2, u'192.168.3.1'), (2, u'192.168.4.1'),
|
||||||
|
(10, u'2404:6800:4004:80c::1'), (10, u'2404:6800:4004:80c::2'),
|
||||||
|
(10, u'2404:6800:4004:80c::3'), (10, u'2404:6800:4004:80c::4')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_islocal():
|
||||||
|
assert sshuttle.helpers.islocal("127.0.0.1", socket.AF_INET)
|
||||||
|
assert not sshuttle.helpers.islocal("192.0.2.1", socket.AF_INET)
|
||||||
|
assert sshuttle.helpers.islocal("::1", socket.AF_INET6)
|
||||||
|
assert not sshuttle.helpers.islocal("2001:db8::1", socket.AF_INET6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_family_ip_tuple():
|
||||||
|
assert sshuttle.helpers.family_ip_tuple("127.0.0.1") \
|
||||||
|
== (socket.AF_INET, "127.0.0.1")
|
||||||
|
assert sshuttle.helpers.family_ip_tuple("192.168.2.6") \
|
||||||
|
== (socket.AF_INET, "192.168.2.6")
|
||||||
|
assert sshuttle.helpers.family_ip_tuple("::1") \
|
||||||
|
== (socket.AF_INET6, "::1")
|
||||||
|
assert sshuttle.helpers.family_ip_tuple("2404:6800:4004:80c::1") \
|
||||||
|
== (socket.AF_INET6, "2404:6800:4004:80c::1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_family_to_string():
|
||||||
|
assert sshuttle.helpers.family_to_string(socket.AF_INET) == "AF_INET"
|
||||||
|
assert sshuttle.helpers.family_to_string(socket.AF_INET6) == "AF_INET6"
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
expected = "1"
|
||||||
|
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1"
|
||||||
|
else:
|
||||||
|
expected = 'AddressFamily.AF_UNIX'
|
||||||
|
assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected
|
145
sshuttle/tests/test_methods_nat.py
Normal file
145
sshuttle/tests/test_methods_nat.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import pytest
|
||||||
|
from mock import Mock, patch, call
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_supported_features():
|
||||||
|
method = get_method('nat')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
assert not features.ipv6
|
||||||
|
assert not features.udp
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tcp_dstip():
|
||||||
|
sock = Mock()
|
||||||
|
sock.getsockopt.return_value = struct.pack(
|
||||||
|
'!HHBBBB', socket.ntohs(socket.AF_INET), 1024, 127, 0, 0, 1)
|
||||||
|
method = get_method('nat')
|
||||||
|
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
||||||
|
assert sock.mock_calls == [call.getsockopt(0, 80, 16)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_recv_udp():
|
||||||
|
sock = Mock()
|
||||||
|
sock.recvfrom.return_value = "11111", "127.0.0.1"
|
||||||
|
method = get_method('nat')
|
||||||
|
result = method.recv_udp(sock, 1024)
|
||||||
|
assert sock.mock_calls == [call.recvfrom(1024)]
|
||||||
|
assert result == ("127.0.0.1", None, "11111")
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_udp():
|
||||||
|
sock = Mock()
|
||||||
|
method = get_method('nat')
|
||||||
|
method.send_udp(sock, None, "127.0.0.1", "22222")
|
||||||
|
assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_tcp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('nat')
|
||||||
|
method.setup_tcp_listener(listener)
|
||||||
|
assert listener.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_udp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('nat')
|
||||||
|
method.setup_udp_listener(listener)
|
||||||
|
assert listener.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_settings():
|
||||||
|
method = get_method('nat')
|
||||||
|
method.check_settings(True, True)
|
||||||
|
method.check_settings(False, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_firewall_command():
|
||||||
|
method = get_method('nat')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.methods.nat.ipt')
|
||||||
|
@patch('sshuttle.methods.nat.ipt_ttl')
|
||||||
|
@patch('sshuttle.methods.nat.ipt_chain_exists')
|
||||||
|
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||||
|
mock_ipt_chain_exists.return_value = True
|
||||||
|
method = get_method('nat')
|
||||||
|
assert method.name == 'nat'
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(10, u'2404:6800:4004:80c::33')],
|
||||||
|
10,
|
||||||
|
[(10, 64, False, u'2404:6800:4004:80c::'),
|
||||||
|
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
||||||
|
True)
|
||||||
|
assert str(excinfo.value) \
|
||||||
|
== 'Address family "AF_INET6" unsupported by nat method_name'
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
|
assert mock_ipt.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 nat method_name'
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == []
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
|
assert mock_ipt.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_ipt_chain_exists.mock_calls == [
|
||||||
|
call(2, 'nat', 'sshuttle-1025')
|
||||||
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == [
|
||||||
|
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||||
|
'--dest', u'1.2.3.0/24', '-p', 'tcp', '--to-ports', '1025'),
|
||||||
|
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
|
||||||
|
'--dest', u'1.2.3.33/32', '-p', 'udp',
|
||||||
|
'--dport', '53', '--to-ports', '1027')
|
||||||
|
]
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(2, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-F', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-X', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-N', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-F', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-p', 'tcp')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
|
method.setup_firewall(1025, 0, [], 2, [], False)
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(2, 'nat', 'sshuttle-1025')
|
||||||
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(2, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-F', 'sshuttle-1025'),
|
||||||
|
call(2, 'nat', '-X', 'sshuttle-1025')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
160
sshuttle/tests/test_methods_pf.py
Normal file
160
sshuttle/tests/test_methods_pf.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import pytest
|
||||||
|
from mock import Mock, patch, call, ANY
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_supported_features():
|
||||||
|
method = get_method('pf')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
assert not features.ipv6
|
||||||
|
assert not features.udp
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tcp_dstip():
|
||||||
|
sock = Mock()
|
||||||
|
sock.getpeername.return_value = ("127.0.0.1", 1024)
|
||||||
|
sock.getsockname.return_value = ("127.0.0.2", 1025)
|
||||||
|
sock.family = socket.AF_INET
|
||||||
|
|
||||||
|
firewall = Mock()
|
||||||
|
firewall.pfile.readline.return_value = \
|
||||||
|
"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n"
|
||||||
|
|
||||||
|
method = get_method('pf')
|
||||||
|
method.set_firewall(firewall)
|
||||||
|
assert method.get_tcp_dstip(sock) == ('127.0.0.3', 1026)
|
||||||
|
|
||||||
|
assert sock.mock_calls == [
|
||||||
|
call.getpeername(),
|
||||||
|
call.getsockname(),
|
||||||
|
]
|
||||||
|
assert firewall.mock_calls == [
|
||||||
|
call.pfile.write('QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'),
|
||||||
|
call.pfile.flush(),
|
||||||
|
call.pfile.readline()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_recv_udp():
|
||||||
|
sock = Mock()
|
||||||
|
sock.recvfrom.return_value = "11111", "127.0.0.1"
|
||||||
|
method = get_method('pf')
|
||||||
|
result = method.recv_udp(sock, 1024)
|
||||||
|
assert sock.mock_calls == [call.recvfrom(1024)]
|
||||||
|
assert result == ("127.0.0.1", None, "11111")
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_udp():
|
||||||
|
sock = Mock()
|
||||||
|
method = get_method('pf')
|
||||||
|
method.send_udp(sock, None, "127.0.0.1", "22222")
|
||||||
|
assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_tcp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('pf')
|
||||||
|
method.setup_tcp_listener(listener)
|
||||||
|
assert listener.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_udp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('pf')
|
||||||
|
method.setup_udp_listener(listener)
|
||||||
|
assert listener.mock_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_settings():
|
||||||
|
method = get_method('pf')
|
||||||
|
method.check_settings(True, True)
|
||||||
|
method.check_settings(False, True)
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.methods.pf.sys.stdout')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_firewall_command(mock_pf_get_dev, mock_ioctl, mock_stdout):
|
||||||
|
method = get_method('pf')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
|
||||||
|
socket.AF_INET, socket.IPPROTO_TCP,
|
||||||
|
"127.0.0.1", 1025, "127.0.0.2", 1024)
|
||||||
|
assert method.firewall_command(command)
|
||||||
|
|
||||||
|
assert mock_pf_get_dev.mock_calls == [call()]
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 3226747927, ANY),
|
||||||
|
]
|
||||||
|
assert mock_stdout.mock_calls == [
|
||||||
|
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
|
||||||
|
call.flush(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME - test fails with platform=='darwin' due re.search not liking Mock
|
||||||
|
# objects.
|
||||||
|
@patch('sshuttle.methods.pf.sys.platform', 'not_darwin')
|
||||||
|
@patch('sshuttle.methods.pf.pfctl')
|
||||||
|
@patch('sshuttle.methods.pf.ioctl')
|
||||||
|
@patch('sshuttle.methods.pf.pf_get_dev')
|
||||||
|
def test_setup_firewall(mock_pf_get_dev, mock_ioctl, mock_pfctl):
|
||||||
|
method = get_method('pf')
|
||||||
|
assert method.name == 'pf'
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(10, u'2404:6800:4004:80c::33')],
|
||||||
|
10,
|
||||||
|
[(10, 64, False, u'2404:6800:4004:80c::'),
|
||||||
|
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
||||||
|
True)
|
||||||
|
assert str(excinfo.value) \
|
||||||
|
== 'Address family "AF_INET6" unsupported by pf method_name'
|
||||||
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == []
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(2, u'1.2.3.33')],
|
||||||
|
2,
|
||||||
|
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
||||||
|
True)
|
||||||
|
assert str(excinfo.value) == 'UDP not supported by pf method_name'
|
||||||
|
assert mock_pf_get_dev.mock_calls == []
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == []
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(2, u'1.2.3.33')],
|
||||||
|
2,
|
||||||
|
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
||||||
|
False)
|
||||||
|
assert mock_ioctl.mock_calls == [
|
||||||
|
call(mock_pf_get_dev(), 3295691827, ANY),
|
||||||
|
call(mock_pf_get_dev(), 3424666650, ANY),
|
||||||
|
call(mock_pf_get_dev(), 3424666650, ANY),
|
||||||
|
call(mock_pf_get_dev(), 3295691827, ANY),
|
||||||
|
call(mock_pf_get_dev(), 3424666650, ANY),
|
||||||
|
call(mock_pf_get_dev(), 3424666650, ANY),
|
||||||
|
]
|
||||||
|
# FIXME - needs more work
|
||||||
|
# print(mock_pfctl.mock_calls)
|
||||||
|
# assert mock_pfctl.mock_calls == []
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
|
||||||
|
method.setup_firewall(1025, 0, [], 2, [], False)
|
||||||
|
assert mock_ioctl.mock_calls == []
|
||||||
|
assert mock_pfctl.mock_calls == [call('-a sshuttle -F all')]
|
||||||
|
mock_pf_get_dev.reset_mock()
|
||||||
|
mock_pfctl.reset_mock()
|
||||||
|
mock_ioctl.reset_mock()
|
272
sshuttle/tests/test_methods_tproxy.py
Normal file
272
sshuttle/tests/test_methods_tproxy.py
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
from mock import Mock, patch, call
|
||||||
|
|
||||||
|
from sshuttle.methods import get_method
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_supported_features():
|
||||||
|
method = get_method('tproxy')
|
||||||
|
features = method.get_supported_features()
|
||||||
|
assert features.ipv6
|
||||||
|
assert features.udp
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tcp_dstip():
|
||||||
|
sock = Mock()
|
||||||
|
sock.getsockname.return_value = ('127.0.0.1', 1024)
|
||||||
|
method = get_method('tproxy')
|
||||||
|
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
|
||||||
|
assert sock.mock_calls == [call.getsockname()]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("sshuttle.methods.tproxy.recv_udp")
|
||||||
|
def test_recv_udp(mock_recv_udp):
|
||||||
|
mock_recv_udp.return_value = ("127.0.0.1", "127.0.0.2", "11111")
|
||||||
|
|
||||||
|
sock = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
result = method.recv_udp(sock, 1024)
|
||||||
|
assert sock.mock_calls == []
|
||||||
|
assert mock_recv_udp.mock_calls == [call(sock, 1024)]
|
||||||
|
assert result == ("127.0.0.1", "127.0.0.2", "11111")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("sshuttle.methods.socket.socket")
|
||||||
|
def test_send_udp(mock_socket):
|
||||||
|
sock = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
method.send_udp(sock, "127.0.0.2", "127.0.0.1", "2222222")
|
||||||
|
assert sock.mock_calls == []
|
||||||
|
assert mock_socket.mock_calls == [
|
||||||
|
call(sock.family, 2),
|
||||||
|
call().setsockopt(1, 2, 1),
|
||||||
|
call().setsockopt(0, 19, 1),
|
||||||
|
call().bind('127.0.0.2'),
|
||||||
|
call().sendto("2222222", '127.0.0.1'),
|
||||||
|
call().close()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_tcp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
method.setup_tcp_listener(listener)
|
||||||
|
assert listener.mock_calls == [
|
||||||
|
call.setsockopt(0, 19, 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_udp_listener():
|
||||||
|
listener = Mock()
|
||||||
|
method = get_method('tproxy')
|
||||||
|
method.setup_udp_listener(listener)
|
||||||
|
assert listener.mock_calls == [
|
||||||
|
call.setsockopt(0, 19, 1),
|
||||||
|
call.v4.setsockopt(0, 20, 1),
|
||||||
|
call.v6.setsockopt(41, 74, 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_settings():
|
||||||
|
method = get_method('tproxy')
|
||||||
|
method.check_settings(True, True)
|
||||||
|
method.check_settings(False, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_firewall_command():
|
||||||
|
method = get_method('tproxy')
|
||||||
|
assert not method.firewall_command("somthing")
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sshuttle.methods.tproxy.ipt')
|
||||||
|
@patch('sshuttle.methods.tproxy.ipt_ttl')
|
||||||
|
@patch('sshuttle.methods.tproxy.ipt_chain_exists')
|
||||||
|
def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
|
||||||
|
mock_ipt_chain_exists.return_value = True
|
||||||
|
method = get_method('tproxy')
|
||||||
|
assert method.name == 'tproxy'
|
||||||
|
|
||||||
|
# IPV6
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1024, 1026,
|
||||||
|
[(10, u'2404:6800:4004:80c::33')],
|
||||||
|
10,
|
||||||
|
[(10, 64, False, u'2404:6800:4004:80c::'),
|
||||||
|
(10, 128, True, u'2404:6800:4004:80c::101f')],
|
||||||
|
True)
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(10, 'mangle', 'sshuttle-m-1024'),
|
||||||
|
call(10, 'mangle', 'sshuttle-t-1024'),
|
||||||
|
call(10, 'mangle', 'sshuttle-d-1024')
|
||||||
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-m-1024'),
|
||||||
|
call(10, 'mangle', '-X', 'sshuttle-m-1024'),
|
||||||
|
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-t-1024'),
|
||||||
|
call(10, 'mangle', '-X', 'sshuttle-t-1024'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-d-1024'),
|
||||||
|
call(10, 'mangle', '-X', 'sshuttle-d-1024'),
|
||||||
|
call(10, 'mangle', '-N', 'sshuttle-m-1024'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-m-1024'),
|
||||||
|
call(10, 'mangle', '-N', 'sshuttle-d-1024'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-d-1024'),
|
||||||
|
call(10, 'mangle', '-N', 'sshuttle-t-1024'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-t-1024'),
|
||||||
|
call(10, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'),
|
||||||
|
call(10, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1024'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK',
|
||||||
|
'--set-mark', '1'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||||
|
'-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket',
|
||||||
|
'-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
|
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1',
|
||||||
|
'--dest', u'2404:6800:4004:80c::33/32',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
|
'-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
|
'-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
|
'-m', 'udp', '-p', 'udp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
|
||||||
|
'--dest', u'2404:6800:4004:80c::101f/128',
|
||||||
|
'-m', 'udp', '-p', 'udp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
|
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
|
'-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
|
'-m', 'tcp', '-p', 'tcp', '--on-port', '1024'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
|
||||||
|
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
|
'-m', 'udp', '-p', 'udp'),
|
||||||
|
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--on-port', '1024')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
|
method.setup_firewall(1025, 0, [], 10, [], True)
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(10, 'mangle', 'sshuttle-m-1025'),
|
||||||
|
call(10, 'mangle', 'sshuttle-t-1025'),
|
||||||
|
call(10, 'mangle', 'sshuttle-d-1025')
|
||||||
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(10, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
|
call(10, 'mangle', '-X', 'sshuttle-m-1025'),
|
||||||
|
call(10, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(10, 'mangle', '-X', 'sshuttle-t-1025'),
|
||||||
|
call(10, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(10, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
|
# IPV4
|
||||||
|
|
||||||
|
method.setup_firewall(
|
||||||
|
1025, 1027,
|
||||||
|
[(2, u'1.2.3.33')],
|
||||||
|
2,
|
||||||
|
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
|
||||||
|
True)
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(2, 'mangle', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', 'sshuttle-d-1025')
|
||||||
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(2, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-X', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-X', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(2, 'mangle', '-X', 'sshuttle-d-1025'),
|
||||||
|
call(2, 'mangle', '-N', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-N', 'sshuttle-d-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(2, 'mangle', '-N', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-d-1025',
|
||||||
|
'-j', 'MARK', '--set-mark', '1'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
||||||
|
'-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket',
|
||||||
|
'-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
|
'--set-mark', '1', '--dest', u'1.2.3.33/32',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
|
||||||
|
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
|
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
||||||
|
'-m', 'tcp', '-p', 'tcp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
|
||||||
|
'-m', 'tcp', '-p', 'tcp', '--on-port', '1025'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
|
||||||
|
'--set-mark', '1', '--dest', u'1.2.3.0/24',
|
||||||
|
'-m', 'udp', '-p', 'udp'),
|
||||||
|
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY',
|
||||||
|
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24',
|
||||||
|
'-m', 'udp', '-p', 'udp', '--on-port', '1025')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
||||||
|
|
||||||
|
method.setup_firewall(1025, 0, [], 2, [], True)
|
||||||
|
assert mock_ipt_chain_exists.mock_calls == [
|
||||||
|
call(2, 'mangle', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', 'sshuttle-d-1025')
|
||||||
|
]
|
||||||
|
assert mock_ipt_ttl.mock_calls == []
|
||||||
|
assert mock_ipt.mock_calls == [
|
||||||
|
call(2, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-X', 'sshuttle-m-1025'),
|
||||||
|
call(2, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-X', 'sshuttle-t-1025'),
|
||||||
|
call(2, 'mangle', '-F', 'sshuttle-d-1025'),
|
||||||
|
call(2, 'mangle', '-X', 'sshuttle-d-1025')
|
||||||
|
]
|
||||||
|
mock_ipt_chain_exists.reset_mock()
|
||||||
|
mock_ipt_ttl.reset_mock()
|
||||||
|
mock_ipt.reset_mock()
|
Reference in New Issue
Block a user