Compare commits

..

268 Commits

Author SHA1 Message Date
780997e8a7 New release 0.73. 2015-11-27 14:22:09 +11:00
d41579c265 Add comment about IPFW support. 2015-11-27 14:18:16 +11:00
974f9aee81 Remove legacy Debian packaging.
This needs to be redone; will do so at a later stage.
2015-11-27 14:13:45 +11:00
4252e81fb0 Update documentation. 2015-11-27 14:13:18 +11:00
7e10fc0756 Add to debugging messages. 2015-11-25 13:06:43 +11:00
2c2ee12e58 Formatting change. 2015-11-25 12:59:48 +11:00
256ed7d244 Fix reversed debug messages. 2015-11-25 12:59:17 +11:00
151634cd8c Fix typo setting up UDP. 2015-11-25 12:58:39 +11:00
c0748c2388 Support IPV6 DNS servers.
Closes #28.
2015-11-24 12:23:17 +11:00
71d46d77bf Add sock paramater to Handler callbacks
As Handler objects can have multiple sockets, we need to know which one
was involved in the incoming event.
2015-11-24 12:19:31 +11:00
c1083e983f Pass correct method back from firewall.
Don't pass auto back.
2015-11-24 12:08:12 +11:00
9944b97629 Remove legacy MACOSX files.
Broken and not been maintained in some time. See #21.
2015-11-24 07:17:19 +11:00
eaad54f68b Add FIXME comment. 2015-11-18 20:08:15 +11:00
6ebf76a5d8 Avoid hardcoding packed address lengths. 2015-11-18 20:07:41 +11:00
51eb7862c4 Fix tests under PyPy. 2015-11-18 20:07:03 +11:00
75b6865a1d Tests for pf method. 2015-11-17 20:52:31 +11:00
e3a1c56e54 Add more methods tests.
Fix bug in tproxy recv_udp() method.
2015-11-17 17:55:30 +11:00
99050aacb3 Fix for Python3.5. 2015-11-17 13:14:28 +11:00
021e6f57af Add more tests. 2015-11-17 13:08:12 +11:00
9cc6d63684 Fix firewall tests. 2015-11-17 12:46:35 +11:00
43566ebda6 Remove unused import. 2015-11-17 10:58:44 +11:00
537899c1df Remove unused function. 2015-11-17 10:58:29 +11:00
641a193d3d Use readline instead of next. 2015-11-17 09:39:53 +11:00
71d17e449e Disable Python 2.6 tests
importlib is Python 2.7 only.
2015-11-17 09:33:46 +11:00
9d443e4155 Don't use nested.
Is Python 2.x only. Not supported under Python 3.x.
2015-11-17 09:32:40 +11:00
cf0aaa7134 Fix PYTHONPATH for tests. 2015-11-17 09:28:58 +11:00
54de23aae3 Add firewall tests. 2015-11-17 09:19:20 +11:00
ac723694bf Restructure code
Pull out firewall methods code into seperate files.

Fix problems starting with method=='auto'; we were making decisions
based on the method, before the method had been finalized by the
firewall.

Only very basic testing so far. What could go wrong?
2015-11-16 18:55:56 +11:00
bcd3205db1 Fix passing latency_control to server. 2015-11-16 11:32:17 +11:00
a651d748cd Remove unused code. 2015-11-16 09:23:24 +11:00
fe48c7c026 Fix PEP8 issues. 2015-11-16 09:10:02 +11:00
4bd6ec8f01 Remove broken su fallback.
Was broken by passing environment variable PYTHONPATH to process. Will
fix this if there is a use case for it.
2015-11-16 09:09:02 +11:00
ba1cf58a6c Add Python 3.5 support. 2015-11-16 09:09:01 +11:00
dd8e68b6dc More formatting fixes. 2015-11-15 17:17:16 +11:00
1f2117917f Fix up formatting. 2015-11-15 17:10:04 +11:00
e6f2395dac Fixup PEP8 issues. 2015-11-15 16:49:20 +11:00
d4f10b232a Restructure code
* Make compatible with setuptools.
* Load modules via ssh into separate modules, not the one name space.
2015-11-15 16:45:26 +11:00
41b8ad4c97 Merge pull request #25 from vieira/ns-hosts
Import resolvconf_nameservers, fix wrong types
2015-11-11 13:09:59 +11:00
a82224c141 Import resolvconf_nameservers, fix wrong types
Add resolvconf_nameservers to the list of functions imported from
helpers.
Fixed an instance where the method client.main was being called with
ns_hosts (string obtained from optional argument --ns-hosts) instead of
nslist (list of tuples that was already being passed to other methods).
Should fix issue #24.
2015-11-08 01:27:10 +00:00
0fb714893a Merge pull request #23 from vieira/ns-hosts
dns: Added --ns-hosts to tunnel only some requests
2015-10-29 13:13:59 +11:00
28be71ef9a Removed commented out code 2015-10-27 17:53:35 +00:00
d2ee34d71c dns: Added --ns-hosts to tunnel only some requests
By default, the --dns flag configures the firewall to only intercept
queries made to the nameservers defined in resolvconf. This flag enables
the user to explicitly specify the nameservers which queries will be
redirected. This can be useful when the local nameserver forwards
queries to some domains to a nameserver on the remote site of the
tunnel.
2015-10-27 17:28:52 +00:00
3cf5002b62 Merge pull request #19 from naclander/patch-1
Remove no-latency-control assertion
2015-09-16 13:08:23 +10:00
f71704f54d Remove no-latency-control assertion
Remove an assertion that would fail when --no-latency-control is set.
2015-09-15 19:30:34 -07:00
ad83059da8 Merge pull request #17 from elasticdog/master
Update ui-macos sources.list with new icon names
2015-09-08 12:00:50 +10:00
d211fc28ee Update ui-macos sources.list with new icon names
I looks like building the app UI for OS X has been broken since
9eced8d049
due to the sources.list.do file still referencing the old .png images.

Without this fix the build will stop at:

    do            chicken-tiny.png
    do: Users/elasticdog/sshuttle/src/ui-macos/chicken-tiny.png: no .do file
    do:         Sshuttle VPN.app: got exit code 1
    do:       Sshuttle VPN.app.zip: got exit code 1
    do:     dist: got exit code 1
    do:   ui-macos/all: got exit code 1
    do: all: got exit code 1
2015-09-07 18:22:30 -07:00
f4dac68dc0 Merge pull request #16 from prutschman/localhost_fix
Don't redirect excluded subsets of included subnets
2015-09-04 17:30:26 +10:00
3a73520310 Don't redirect excluded subsets of included subnets 2015-09-03 21:25:23 -07:00
e127aab776 Merge pull request #14 from reactormonk/patch-1
Switched the ./sshuttle to src/sshuttle
2015-07-26 11:15:56 +10:00
5f90ee1f04 Switched the ./sshuttle to src/sshuttle 2015-07-25 17:38:42 +02:00
d70b5f2b89 Merge pull request #13 from shaiay/master
Fixed issue #12
2015-07-23 08:08:09 +10:00
3f2de26f67 Fixed issue #12
family should be an integer. fixed parsing routes
2015-07-22 22:52:25 +03:00
53d5260f8f Merge pull request #11 from douglas/master
Fix the excludes rule for OS X Yosemite and OS X El Captain
2015-06-19 17:34:14 +10:00
f870ceba00 Fix the excludes rule for OS X Yosemite and OS X El Captain
Without this fix, the rdr rule is executed sending the packages that
should be excluded to the ssh tunnel.

What I did was make sure that the packages that are going to the
excluded subnets are processed first and only after that, the remaining
packages will be sent to the ssh tunnel.

Thanks Warr1024 on #openssh channel in freenode for telling me about
the quick keyword and the rest of guys in the channel who tried to help.
2015-06-18 18:09:18 -03:00
a38963301e Merge pull request #10 from jbd/patch-1
Check for fileno attribute in _tty_width function
2015-05-13 11:01:05 +10:00
jbd
bbd54e150d Check for fileno attribute in _tty_width function
When using Options parser within a unittest.TextTestRunner with buffering enabled (buffer=True), it fails with: 
AttributeError: StringIO instance has no attribute 'fileno'

This change will prevent this kind of error.
2015-05-12 16:43:38 +02:00
00f20657e3 Merge pull request #9 from scommab/patch-1
Make firewall.py use the right params for islocal
2015-05-03 14:42:23 +10:00
84b30be904 Make firewall.py use the right params for islocal 2015-05-01 23:36:08 -07:00
5825dddb02 Merge pull request #8 from nanoant/patch/osx-improvements
Patch/osx improvements
2015-04-21 09:49:18 +10:00
9eced8d049 OSX: New Retina compatible menu & app icon
This icons are using scale independent PDF template images which make menu item
icon look great on both Yosemite light & dark theme. Also adding new flatter
and higher resolution app icon.
2015-04-20 20:32:02 +02:00
fecb53413d OSX: Remove status item on application quit
This ensures application is not leaving empty status item.
2015-04-20 20:23:20 +02:00
1b1ed4d495 OSX: Improve app startup time
Importing everything (*) from AppKit takes a while, since we got 3 scripts
doing that, startup could take up to few seconds. This change makes script
import only what they need, improving startup time to fraction of second.
2015-04-20 18:53:06 +02:00
b19272a67a Merge pull request #7 from xtaran/master
Fix the most blatant issues of the generated .deb
2015-04-20 10:39:55 +10:00
bc2a0b7fbc Fix path to main.py in .deb 2015-04-18 14:46:55 +02:00
6a96ace497 autossh is not required but nice to have 2015-04-18 14:40:00 +02:00
163aab2ca1 Fix typo in long description of .deb 2015-04-18 14:31:30 +02:00
964977220e Change .deb section from utils to net 2015-04-18 14:30:35 +02:00
db67834164 Add missing dependency on iptables in the .deb 2015-04-18 14:30:30 +02:00
1bc2f84d16 Use a less confusing version for .deb packages built from git 2015-04-18 14:30:30 +02:00
a229fc020c Properly separate short and long description in .deb 2015-04-18 14:30:30 +02:00
d6e7a9b6ad Update homepage header of .deb 2015-04-18 14:30:30 +02:00
e6ca7148fa Fix formatting of versioned dependency in the .deb 2015-04-18 14:30:21 +02:00
95529a5137 Don't include MacOS X stuff in .deb 2015-04-18 14:30:21 +02:00
93c4af6fc8 There's no need to have hard dependency on a init system in the .deb
Otherwise the package is only installable on current Ubuntu releases
and neither on future Ubuntu releases (which will use systemd) nor on
Debian and other Debian derivatives (where the administrator can
decided which init system is used).
2015-04-18 14:30:16 +02:00
2ca9aaa450 The .deb is and needs to be architecture-independent 2015-04-18 14:14:33 +02:00
2cfc39fac8 Fix UDP channel expiration. 2015-04-12 09:59:49 +10:00
29819ea0af Merge pull request #6 from lkorth/patch-1
Fix clone url in README
2015-04-11 10:15:20 +10:00
e43a40565b Fix clone url in README 2015-04-09 09:22:31 -07:00
57d1cb1e11 Merge pull request #5 from seanzxx/yosemite_support_sudo_fix
fix sudo issue in yosemite
2015-03-23 14:11:16 +11:00
6e32d1445a add -e/-d support 2015-03-21 22:43:12 -07:00
bdad253ef5 fix mistake 2015-03-21 15:36:42 -07:00
49c55f6825 use -E/-X to enable/disable pf on yosemite 2015-03-21 15:28:17 -07:00
1874aaceb4 refine firewall initlization 2015-03-21 00:00:15 -07:00
4c31bc02a4 add anchor rule directly 2015-03-20 18:21:00 -07:00
84047089a9 fix sudo issue 2015-03-19 02:43:11 -07:00
8be9270fdb Merge pull request #4 from seanzxx/yosemite_support
Yosemite support
2015-03-19 09:55:39 +11:00
10dc229125 fix bootstrapping issue when pf started before 2015-03-18 09:25:41 -07:00
cd77ad5e7b refine error message 2015-03-15 22:53:08 -07:00
c13cb9b8ca optimize the ctypes import 2015-03-15 22:45:32 -07:00
0fe48a4682 initial support for pf in yosemite 2015-03-15 22:34:40 -07:00
6121a6dca3 sshuttle.md: fix whitespace issues. 2014-12-16 14:06:13 +11:00
c576682caf sshuttle.md: document Internet Sharing incompatibility 2014-12-16 14:04:25 +11:00
343905784b Added --exclude-from feature.
(Slightly modified by apenwarr)
2014-10-06 13:04:33 +11:00
91d705c24f Document missing --dns option in sshuttle manpage 2014-10-06 13:01:31 +11:00
e5251969b0 firewall.py: catch SIGINT and SIGTERM too.
There were still a few conditions under some OSes that would cause
firewall.py to terminate without cleaning up the firewall settings.  'pkill
sshuttle' was one of them.  Ignore a couple more signals to further ensure a
correct cleanup.

(This only affects sshuttle --firewall, which is a subprocess of the main
sshuttle process.  The firewall is supposed to exit automatically whenever
the client exits, and so far that part seems to work reliably.)
2014-10-06 13:00:57 +11:00
b8e150fc4d Use python-config to compile with latest Python version.
For OS X systems without Python 2.5, runpython.c does not compile.
Use python-config to get the paths for the latest version.
2014-10-03 14:58:26 -07:00
36378efe5e Revert Debian package specific change.
This is required so sshuttle can be run from git repository.

The way the Debian package is created is non-standard, and probably
needs redoing anyway.
2014-09-23 11:14:56 +10:00
cba8b261c6 Use the new arguments from redo v0.10.
(apenwarr: also updates to the matching, latest minimal/do)
2014-09-23 10:14:59 +10:00
39425a03c5 firewall: catch SIGHUP and SIGPIPE.
Not sure if this will fix anything, but it might stop the problem reported
on some MacOS versions where the firewall doesn't get cleaned up correctly.
2014-09-23 10:14:27 +10:00
5a39341d50 ui-macos/main.py: fix wait() to avoid deadlock.
If the subprocess was trying to write to its stdout/stderr, its process
would never actually finish because it was blocked waiting for us to read
it, but we were blocked on waitpid().  Instead, use waitpid(WNOHANG) and
continually read from the subprocess (which should be a blocking operation)
until it exits.
2014-09-23 10:11:13 +10:00
3eef3635ac ipfw: don't use 'log' parameter.
I guess we were causing the kernel to syslog on every single packet on
MacOS.  Oops.
2014-09-23 10:09:16 +10:00
f1c79c7e92 PEP8 fixes. 2014-09-16 10:24:16 +10:00
5529a04cc9 Fix whitespace. 2014-09-15 14:46:45 +10:00
035c5ad7a6 Fix: Use sock for consistency. 2014-09-15 14:44:07 +10:00
c013386ecb If IPv4 bind but IPv6 succeeds don't error. 2014-09-15 14:32:59 +10:00
a33f6199c4 Remove broken IPv6 code. 2014-09-15 14:23:09 +10:00
0f2c249e4d Remove dodgy code. 2014-09-15 14:14:52 +10:00
192e5b36e8 Added some Ubuntu notes 2014-09-15 14:14:52 +10:00
4036b7dfcf Added some requirements 2014-09-15 14:14:52 +10:00
8ec6daf02a Added a shell script to make a .deb package 2014-09-15 14:14:52 +10:00
e2507f86d5 Added a control file for the Debian package 2014-09-15 14:14:52 +10:00
e4fe62de3c Added a sample prefixes file 2014-09-15 14:14:52 +10:00
734f32d112 Sample tunnel configuration 2014-09-15 14:14:52 +10:00
a34e106b55 Changed the sshuttle binary to point to install 2014-09-15 14:14:52 +10:00
e6e80f1f04 Changed the file to be more "canonical" 2014-09-15 14:14:52 +10:00
32865bd2dd Added the PyXAPI requirement to the readme 2014-09-15 14:14:52 +10:00
2f11f50bc2 Adding more robust exit codes 2014-09-15 14:14:52 +10:00
a95491765d Added -s to accept subnets from a config file 2014-09-15 14:14:52 +10:00
d8754dc3a0 First version; still has debugging 2014-09-15 14:14:52 +10:00
3956a5df94 Moved docs out of the src directory 2014-09-15 14:14:52 +10:00
7442eb61e9 Mass relocation of files to their own subdirectory 2014-09-15 14:14:51 +10:00
6107abf10f Fixed a bug where lack of IPv6 destination = fatal
There was a problem where trying to bind .v4 and .v6 listeners would set them
to None if there was nothing to bind (if, say, you weren't binding an IPv6
listener).  Later, the code then would try to call a member function of the
listener.  The member function would not do anything if there was no listener,
but trying to dereference None yielded the broken behavior.
2014-09-15 14:14:51 +10:00
5e8ad544ee TProxy UDP support, including DNS. 2011-08-26 09:53:59 +10:00
20254bab57 TProxy IPv6 support. 2011-07-11 11:20:52 +10:00
f41c6b62e5 TProxy support as well as NAT support. 2011-07-11 11:20:51 +10:00
9a7412c08f More changes to simplify the upcomming IPv6 patch. 2011-07-11 11:20:50 +10:00
c6200eecdc Choose which method to use for intercepting traffic. 2011-07-11 11:16:51 +10:00
55f86a8b3f Rewrite binding code. DNS port may now be different from TCP port. 2011-07-11 11:16:50 +10:00
e7caae8126 Make it clear ports are for IPv4. 2011-07-11 11:16:49 +10:00
4db9b372c2 Make iptables functions work with any table, not just nat. 2011-07-11 11:16:48 +10:00
061e6a0933 Keep track of address family address belongs too. 2011-06-16 14:51:34 +10:00
50849b86b0 This hack is IPv4 specific, ensure it doesn't get used for other
address families.
2011-06-16 14:51:34 +10:00
6b7cf770f5 Improve debugging. 2011-06-16 14:42:15 +10:00
b26e1ee652 Introduce independent_listener, will be used for both IPv4 and IPv6
connections.
2011-06-06 11:14:28 +10:00
6500067905 When DNS response received, MUX channel no longer required. Delete it. 2011-06-06 11:12:23 +10:00
50c2b86f15 Rename onaccept to onaccept_tcp as it is tcp specific. 2011-06-06 10:54:57 +10:00
97dca42291 Rename dnslistener to dns_listener for consistency with tcp_listener. 2011-06-06 10:44:38 +10:00
6e53b07002 Rename listener to tcp_listener, as it is TCP specific. 2011-06-06 10:43:39 +10:00
08bd1dca46 Rename TCP specific commands to clarify they are TCP specific. 2011-06-06 10:39:50 +10:00
94566b5efc Split expiration into handling into another function. 2011-06-06 10:23:04 +10:00
a8b71f6387 Move nested functions to top level. 2011-05-31 00:42:48 -04:00
4bfcd7091d Send DNS request back on same sock we received it on. 2011-05-31 00:39:17 -04:00
bd489b3319 Pass socket through to handlers. Required for IPv6 support. 2011-05-31 00:39:17 -04:00
8ab5ef283d ssnet.py: deal with a possible connect/getsockopt(SO_ERROR) race.
Seems to affect Linux servers.  Ed Maste says the patch fixes it for him.
2011-05-29 22:42:16 -04:00
e67208a294 helpers.py: errno is used by this module, but not imported. 2011-05-15 17:35:53 -04:00
7859be13c2 ui-macos/bits/runpython.do: skip ppc64 architecture.
I don't have a Mac that can build it.  Hopefully ppc will run fine on ppc64.
2011-05-07 23:19:52 -04:00
f313d50690 ui-macos/bits/runpython.do: report which platforms we're compiling for.
Just as a quick reminder, in case you're building a fat binary and you don't
have all the architectures actually installed.
2011-05-07 23:16:42 -04:00
15e26d2e0e README.md: fix little bug
The ssh hostname should immediately follow the -r parameter.
2011-05-07 23:16:42 -04:00
e2ec475de3 ui-macos/models.py: fix a compatibility problem on MacOS for PPC.
@objc.accessor isn't the right thing to use for a Core Data Validation
function.  Yowee, PyObjc sure is non-obvious.
2011-05-07 23:16:42 -04:00
57e744dadd ./do: use the latest minimal/do from the redo project. 2011-05-03 14:19:45 -07:00
c13be0b817 ui-macos/bits/runpython.do: auto-determine arches to build for.
Some people don't have all of them installed, so auto-detect them by
looking at the available arches in /usr/libexec.
2011-05-03 14:18:37 -07:00
da2c6273f6 Add some friendly info to the README 2011-05-03 14:03:19 -07:00
7712c60c36 Insert two binary NUL bytes (\0) before SSHUTTLE0001 sync string.
...and search for those null bytes before looking for the sync string.

This helps when people have misconfigured .bashrc to print messages even in
non-interactive mode.  (On my Debian Lenny system, .bashrc doesn't seem to
run when you do 'ssh localhost ls', but on MacOS servers, it does.  Hmm...)
2011-05-03 13:59:25 -07:00
65b0390fe9 ssh.py: use 'exec python -c' instead of just 'python -c'.
This gets rid of an extra intermediate sh process on the server that we were
keeping for no good reason, since it would exit as soon as python exited
anyway.
2011-05-03 13:51:09 -07:00
c5834a9773 Handle EHOSTDOWN, ENETDOWN.
Someone on the mailing list reported getting these.

Also centralize the list of these errors, so we don't have different parts
of the code supporting a different subset of them.  Now just use
ssnet.NET_ERRS.
2011-05-03 13:32:25 -07:00
e2474543fc runpython.do: also compile for ppc architecture. 2011-04-24 22:51:27 -04:00
8636378870 Dereference symlink for sshuttle launch script
(Modified slightly by apenwarr)
2011-04-24 22:42:50 -04:00
f5eed4c809 Don't try to connect to remote IPs that start with zero.
For some reason, on Linux servers this returns EINVAL.  I don't like just
treating EINVAL as non-fatal in general, so let's catch this specific case
and ignore it.

Reported by Reza Mohammadi on the mailing list.  Interestingly, it's kind of
hard to trigger this crash since the client would have to request the
connection, and that connection shouldn't exist because the original client
program would have already gotten EINVAL.  But my MacOS machine can generate
such a connection, so a MacOS->Linux sshuttle could trigger this.
2011-04-24 22:15:20 -04:00
783d33cada DNS: auto-retry if we get an error on send/recv to DNS server.
A few people have reported that they have one or more invalid DNS servers in
/etc/resolv.conf, which they don't notice because the normal resolver
library just skips the broken ones.  sshuttle would abort because it got an
unexpected socket error, which isn't so good.
2011-04-06 12:30:12 -04:00
94241b938b On FreeBSD, avoid a crash caused by buggy socket.connect() in python pre-2.5.
Bug reported by Ed Maste.  The fix in later versions of python is documented
here:
http://mail.python.org/pipermail/python-bugs-list/2006-August/034667.html

We're basically just doing the same thing when we see EINVAL.  Note that
this doesn't happen on Linux because connect() is more forgiving.
2011-03-21 03:15:11 -07:00
9031de1527 repr(socket.error) is useless in some versions of python.
So let's use %s instead of %r to print it, so that log messages can be more
useful.  This only affects one message at debug3 for now, so it's not too
exciting.
2011-03-21 03:15:11 -07:00
cfb2592346 server.py: handle (throw away) ECONNREFUSED from the DNS server.
This might happen occasionally on a flakey network.  Reported by Ed Maste.
2011-03-19 22:48:15 -07:00
2e8381ecda hostwatch.py: avoid using /dev/null on the server.
According to at least one report, there are some slightly insane servers out
there that have /dev/null set to non-user-writable.  This is totally broken,
but we want sshuttle to work with as many servers as possible, so let's fake
it up a bit instead.

We don't try to avoid /dev/null on the client; sshuttle needs root access
anyway, and if you're root, you can just fix your stupid /dev/null
permissions.
2011-03-14 18:57:06 -07:00
7d35690e41 ui-macos/clean: fix a GNUism in usage of the 'find' command. 2011-02-28 02:43:00 -08:00
141d9760b9 all.do: add some hints about how to run sshuttle.
This is mostly so that people know how to find the MacOS GUI app, which was
previously rather non-obvious.
2011-02-26 18:16:44 -08:00
0658c85ffe Replace make-based build with redo-based build.
Including a copy of minimal/do as 'do' in the top directory.  To build, just
run './do' or 'make'.

This also builds the ui-macos directory automatically if you're on MacOS.
2011-02-26 18:16:44 -08:00
90a55a33a2 firewall.py: make it super clear when we apply the MacOS fix.
Print a message to stderr, then abort.  But only the first time.
2011-02-26 17:45:27 -08:00
c3399595d2 README/sshuttle.1: add a note about the MacOS kernel bug.
And its side effects.

Reported by David Held / Antonio d'Souza.
2011-02-26 17:23:11 -08:00
6ef9ae1796 firewall.py: iptables: failure to delete a rule isn't always fatal.
If the previous run of sshuttle didn't manage to clean up after itself, it
might have left the sshuttle-12300 chain intact, but the OUTPUT chain might
not refer to it anymore.  That would cause the *next* run of sshuttle to
barf when trying to delete the OUTPUT entry, and then never get to the part
where it just tries to delete the old chain so it can continue.

Now only the last delete command (the one that actually deletes the chain)
is fatal if it fails; the others just print a scary message, but that should
only happen once in your life if you're unlucky.
2011-02-21 03:04:00 -08:00
1ca8aa5b89 server: workaround for idiotic ArchLinux renaming of python to python2.
First try running under python2, then python if that doesn't exist.
2011-02-07 17:18:30 -08:00
a62975e0ce client: workaround for idiotic ArchLinux renaming of python to python2.
First try running under python2, then python if that doesn't exist.
2011-02-07 00:18:58 -08:00
4fde980f46 firewall.py: MacOS: permanently set the net.inet.ip.scopedroute sysctl.
If this sysctl isn't set to 0 at the time your network interface is brought
up, and we later change it, then the MacOS (10.6.6 at least) ARP table gets
totally confused and networking stops working about 15 minutes later, until
you down and re-up the interface.  The symptom is that pings outside your
LAN would give results like this:

    ping: sendto: no route to host

and "arp -a -n" would show *two* entries for your default gateway instead of
just one.

sshuttle was helpfully putting the sysctl back the way it was when it shuts
down, so you would fix your network by downing the interface, so sshuttle
would abort and change the sysctl back, then you would re-up the interface,
then restart sshuttle, and sshuttle would change the sysctl back and restart
the cycle: it would break again a few minutes later.

That's annoying, and it gives sshuttle a bad reputation for being the thing
that breaks your network.  I can't find a *really* good workaround for the
bug, so barring that, let's just permanently set the sysctl to 0 and not
change it back on exit.  That should just leave your computer back how it
worked in MacOS 10.5, as far as I know, which seems harmless.  At least I've
been running my Mac that way for a few days and I haven't seen any
weirdness.

Now, doing *that* would still mean that the first sshuttle session after a
reboot would still break the network, since sysctl changes are lost on
reboot.  Thus, let's be extra hardcore and write it to /etc/sysctl.conf so
that it goes the way we want it after a reboot.  Thus, sshuttle should break
your network at most once.  Which still sucks, but hopefully nobody will
notice.
2011-02-04 21:55:40 -08:00
621997b279 ui-macos: move the noLatencyControl setting to a per-connection setting.
I think some connections you'll want to optimize for latency, and others for
bandwidth.  Probably.

Also, use a dropdown box instead of a checkbox; that way we can make it more
clear what each of the settings means.

While we're here, adjust all the anchor settings for the different display
items so that resizing the dialog box works sensibly.
2011-02-04 21:40:44 -08:00
ca7d38dc1a stresstest.py: a program to create lots and lots of TCP connections.
This version is a bit limited: it always only connects back to itself, which
is always on 127.0.0.1.  It also doesn't really find any problems, other
than odd behaviour when Linux runs out of available port numbers after a
while.
2011-02-04 21:37:22 -08:00
a81972b2b5 Add --wrap option to force channel number wrapping at a lower number.
This makes it easier to actually test what happens when channel numbers wrap
around.  The good news: it works.

However, I did find a bug where sshuttle would die if we completely ran out
of available channel numbers because so many of them were open.  This would
never realistically happen at the default of 65535 channels (we'd run out of
file descriptors first), but it's still a bug, so let's handle it by just
dropping the connection when it happens.
2011-02-02 02:32:46 -08:00
a238f7636c ui-macos: include routing type in each connection title.
This makes it extra clear when a connection is for "all routes" vs. custom
vs. auto.
2011-02-01 03:55:19 -08:00
62e1ac4b46 ui-macos: add checkboxes for --no-latency-control and --dns options. 2011-02-01 03:55:19 -08:00
f2297066e7 Oops, left in a junk option that causes a crash without --dns. 2011-01-26 11:26:35 -08:00
0bf0351d9b Merge branch 'dns'
* dns:
  dns on MacOS: use divert sockets instead of 'fwd' rules.
  client.py: do DNS listener on the same port as the TCP listener.
  Move client._islocal() to helpers.islocal() in preparation for sharing.
  dns: add support for MacOS (but it doesn't work...)
  Oops, dns_done() crashed if the request had already been timed out.
  dns: trim DNS channel handlers after a response, or after a timeout.
  dns: extract 'nameserver' lines from /etc/resolv.conf
  Extremely basic, but functional, DNS proxying support (--dns option)
2011-01-26 05:29:51 -08:00
9731680d2e dns on MacOS: use divert sockets instead of 'fwd' rules.
It turns out diverting UDP sockets is pretty easy compared to TCP (which
makes it all the more embarrassing that they screwed up 'fwd' support for
UDP and not TCP, but oh well).  So let's use divert sockets instead of
transproxy for our DNS packets.

This is a little tricky because we have to do it all in firewall.py, since
divert sockets require root access, and only firewall.py has root access.
2011-01-26 05:25:27 -08:00
88937e148e client.py: do DNS listener on the same port as the TCP listener.
UDP and TCP have separate port namespaces, so to make it easier to keep
track of what's going on, just use the same transproxy port number for both.
We still need two sockets, but now tcpdumps are easier to understand.
2011-01-26 05:25:26 -08:00
7f3c522c56 Move client._islocal() to helpers.islocal() in preparation for sharing. 2011-01-26 05:25:26 -08:00
ebfc3703ec dns: add support for MacOS (but it doesn't work...)
...because stupid MacOS ipfw 'fwd' rules don't work quite right with udp.
It can intercept packets bound for remote hosts, but it doesn't correctly
rewrite the port number from its original to the new socket, so it gets
dropped by the local kernel anyway.

That is, a packet to 1.2.3.4:53 should be redirected to, say,
127.0.0.1:9999, the local DNS listener socket.  But instead, it gets sent to
127.0.0.1:53, which nobody is listening on, so it gets eaten.

Sigh.
2011-01-26 05:25:26 -08:00
760740e9aa Oops, dns_done() crashed if the request had already been timed out. 2011-01-26 05:25:26 -08:00
b570778894 dns: trim DNS channel handlers after a response, or after a timeout.
This avoids memory/socket leaks.
2011-01-26 02:34:46 -08:00
4c5185dc55 dns: extract 'nameserver' lines from /etc/resolv.conf 2011-01-26 02:34:46 -08:00
a2fcb08a2d Extremely basic, but functional, DNS proxying support (--dns option)
Limitations:
- uses a hardcoded DNS server IP on both client and server
- never expires request/response objects, so leaks memory and sockets
- works only with iptables, not with ipfw
2011-01-26 02:34:46 -08:00
e7a19890aa Merge branch 'fullness'
Tests with speedtest.net to a linode.com server:

                       Downstream     Upstream

No sshuttle            1.25 Mbit/s    0.55 Mbit/s
Default                0.75 Mbit/s    0.51 Mbit/s
--no-latency-control   1.25 Mbit/s    0.55 Mbit/s

* fullness:
  man page for the --no-latency-control option.
  options: remove unused 'exe' parameter
  options.py: generate usage string correctly for no-* options.
  Implement the optional fullness checking a bit more like I like it.
  new option to disable fullness checking
2011-01-25 22:11:28 -08:00
d9b1bb52e5 man page for the --no-latency-control option. 2011-01-25 21:30:29 -08:00
a30c4d7ccb options: remove unused 'exe' parameter
The 'exe' parameter was added in the hope of using it for additional
contextual information in the help text that Options generates. It was
till then abandoned and was judged as superflous information.

Remove the 'exe' parameter from Options' constructor.

(copied from the 'bup' project)

Signed-off-by: Gabriel Filion <lelutin@gmail.com>
2011-01-25 21:19:28 -08:00
9877a8d6fb options.py: generate usage string correctly for no-* options.
Signed-off-by: Avery Pennarun <apenwarr@gmail.com>
2011-01-25 21:14:51 -08:00
8fde1155da Implement the optional fullness checking a bit more like I like it.
Looks like it worked before, but personal preference is a killer.

The new name is "--no-latency-control".
2011-01-25 21:07:39 -08:00
fdb7c9b995 new option to disable fullness checking
On high latency links, the PING/PONG round trip triggered by fullness
checking could kill the bandwidth. Disabling it could result in >10x
bandwidth increase in some setups where the existing latency is already high
and the available bandwidth is also high.
2011-01-25 21:05:13 -08:00
675f19f57e Don't die if iptables doesn't have 'ttl match' support.
ttl matching is only needed if your server is the same machine as the
client, which is kind of useless anyway (other than for testing), so there's
no reason for it to be fatal if that doesn't work.

Reported by "Alphazo" on the mailing list, who managed to get sshuttle
working on his Nokia N900 by removing the ttl stuff.
2011-01-25 20:42:10 -08:00
049a0c40ac ui-macos: guess we don't need stupid.py anymore.
It was just a test.
2011-01-22 16:55:18 -08:00
668441adb6 Merge branch 'macapp'
A cute little GUI for sshuttle on MacOS, written using pyobjc.

* macapp:
  ui-macos: call the main binary MacOS/Sshuttle.
  ui-macos/git-export.do: write the generated app to a branch.
  ui-macos/default.app.do: get rid of some duplicated files.
  ui-macos: Actually prompt for passwords instead of assuming a default.
  ui-macos: Don't enable connecting for hosts with Custom but zero subnets.
  ui-macos: Smoother log messages in the log window.
  ui-macos: Notice when we've connected; make debug logs optional.
  ui-macos: Much better connection status reporting.
  ui-macos/run.do: a shortcut for running debug.app.
  ui-macos/*: "a series of unfortunate events."
2011-01-22 16:49:21 -08:00
d4ccd30c24 ui-macos: call the main binary MacOS/Sshuttle.
That way we can 'killall Sshuttle' and so on, and it looks right in the
process list.
2011-01-22 16:44:20 -08:00
522bc23854 ui-macos/git-export.do: write the generated app to a branch. 2011-01-22 16:44:20 -08:00
191d97a897 ui-macos/default.app.do: get rid of some duplicated files. 2011-01-22 16:44:15 -08:00
da7a490cd7 ui-macos: Actually prompt for passwords instead of assuming a default. 2011-01-22 16:44:11 -08:00
342ce12840 ui-macos: Don't enable connecting for hosts with Custom but zero subnets.
That would cause the sshuttle client to die.
2011-01-22 16:44:05 -08:00
701f59a5b8 ui-macos: Smoother log messages in the log window. 2011-01-22 16:44:01 -08:00
9f7b2bb4f6 ui-macos: Notice when we've connected; make debug logs optional. 2011-01-22 16:43:48 -08:00
d301184184 ui-macos: Much better connection status reporting. 2011-01-22 16:43:44 -08:00
659a57beb2 ui-macos/run.do: a shortcut for running debug.app. 2011-01-22 16:43:09 -08:00
c70b9937df ui-macos/*: "a series of unfortunate events."
Just kidding.  This is a squash of a whole bunch of unlabeled temporary
commits that I produced over the last couple of weeks while writing a UI
for MacOS while riding on airplanes and sitting in airports.

So long, batch of useless commits!
2011-01-22 16:43:09 -08:00
415be935d4 options.py: don't die if tty width is set to 0.
This sometimes happens if weird people, such as myself, open a pty without
setting the width field correctly.
2011-01-22 16:42:32 -08:00
d4c9d31068 Oops, we were being overzealous about calling nowrite().
We were doing it as soon as the other end was shut_read, but we didn't
confirm first that the other end's read buffer had been totally emptied.
This caused problems on twitter.com, at least, where they create a zillion
short-lived connections.

I don't actually need these lines at all, as it turns out, because we were
already calling nowrite() correctly on the other end after a buffer got
emptied.
2011-01-12 09:33:56 -08:00
38bb7f3c21 If we get EPIPE on uwrite(), don't close, just do nowrite().
EPIPE doesn't mean the whole socket is dead, it just means we can't write to
it.  Maybe there's still data waiting to be read, though.
2011-01-12 09:19:43 -08:00
b7f1530aef Remember which syscall got a particular stream-killing error. 2011-01-12 09:18:46 -08:00
973d5a95a1 man page update for daemonization options. 2011-01-01 00:32:37 -08:00
95ab6e7119 ssyslog.py: use daemon.notice instead of daemon.info
...MacOS X seems to default (in /etc/syslog.conf) to not logging daemon.info
anywhere.  That kind of defeats the purpose, I think.
2011-01-01 00:28:31 -08:00
e6d7c44e27 Merge branch 'daemon'
* daemon:
  daemonization: make sure the firewall subproc sends to syslog too.
  Rearrange daemonization/syslog stuff and make it more resilient.
  run in background (daemon) and option
2011-01-01 00:22:43 -08:00
5bf6e40682 daemonization: make sure the firewall subproc sends to syslog too. 2011-01-01 00:22:09 -08:00
8a5ae1a40a Rearrange daemonization/syslog stuff and make it more resilient.
Rename --background to -D/--daemon, to match other programs (like smbd).

You can now have --syslog even without --daemon.

Avoid using atexit(); try/finally is better.

Don't just close stderr; we'll end up eating error output from ssh!
Instead, redirect stderr to a 'logger' subprocess that will send to syslog.

Delay redirecting stderr until after we know we're daemonizing, so handy
error messages can go to stderr instead of syslog.

Make pidfile stuff more resilient: support already-existing files, files
with strict permissions, outdated files containing an already-dead pid.  Add
a --pidfile option to let you specify the pidfile path.

chdir("/") while daemonizing, so that the filesystem containing $PWD can
later be unmounted without killing the daemon.

fw.done() can't wait on the firewall subprocess on exit when daemonized; we
no longer are the parent of that process.
2010-12-31 23:55:19 -08:00
651b607361 If ssh dies right after starting, we might get ECONNRESET.
Turn it into a nicer-looking fatal error instead of an exception dump.
2010-12-31 23:46:47 -08:00
dc9a5e63c7 Minor tweak to help for the python= option. 2010-12-31 23:46:05 -08:00
33bc55be27 Merge branch 'closing'
* closing:
  Correctly close server connection when client disconnects.
  "Too many open files" shouldn't be a fatal condition.
2010-12-31 22:12:54 -08:00
c3204d2728 Correctly close server connection when client disconnects.
When the server disconnected, we were forwarding that information to the
client.  But we weren't forwarding back the other way when the client
disconnected since there was no callback in place to do that.

Relatedly, when we failed entirely to connect to the server, we didn't notify the
client right away.  Now we do.

Thanks to 'Roger' on the mailing list for pointing out these bugs.
2010-12-31 21:59:36 -08:00
b1edb226a5 "Too many open files" shouldn't be a fatal condition.
It can happen if there are too many sockets open.  If that happens, just
throw away any connections that arrive in the meantime instead of aborting
completely.
2010-12-31 21:32:51 -08:00
7fa1c3c4e4 Listen on localhost:0 instead of 0.0.0.0:0 by default.
This avoids any possible problem caused by other people on your network
using you as a proxy.  If you want to allow this, you can force it back to
the old way using the --listen option.

Thanks to 'tass' on github for reporting portscans that revealed this
potential security problem.
2010-12-31 21:22:40 -08:00
cca69eb496 Don't allow proxying of connections to the proxy port.
Add some cleverness for breaking infinite loops.  Previously we'd only
detect it successfully if you connected to exactly the same IP as we were
listening on, but that was unreliable if we're listening on 0.0.0.0 and you
connected to one of the IP addresses we haven't heard of.

Now, if you try to connect to our listen port on *any* IP, we try binding to
that IP as a local socket; if it works, that's a local IP, and therefore
it's our socket, so reject the connection.  If it doesn't work, it's a
remote IP, so forward it along.

Thanks to 'tass' on github for noticing the problem.
2010-12-31 21:22:32 -08:00
91f65132be Get rid of ugly quotes on "Accept:" log messages. 2010-12-31 20:54:46 -08:00
2ef3a301fb run in background (daemon) and option 2010-12-12 12:08:54 +08:00
41fd0348eb Fix a bug when packets are received on a channel after it closes.
Reported by cbowns.
2010-12-11 17:27:12 -08:00
1907048dad Remove the never-used and misleading CMD_CLOSE. 2010-12-09 19:20:09 -08:00
82e1d1c166 Fix memory leak of MuxWrapper object.
(Note by apenwarr: I used Roger's original patch as the basis for this one,
but implemented it a different way.  All errors are thus my fault, but Roger
gets the credit for actually tracking down the circular reference that
caused the memory leak.)
2010-12-09 19:20:07 -08:00
a497132c01 Add debug messages for counting SockWrapper objects.
You can use this to confirm that a memory leak exists.
2010-12-09 19:20:06 -08:00
7354600849 Fix a socket leak: delete object after close on both direction.
(Note by apenwarr: seems to still work for me.  The reason the
problem occurred is that reassigning 'handlers' doesn't change it in its
parent; it creates a whole new list, and the caller still owns the old one
with all the dead sockets in it.  The problem seems to have been introduced
in commit 84376284db when I factored the
runonce() functionality out of the client and server but didn't notice this
reassignment.)
2010-12-09 19:20:01 -08:00
918725c485 Oops, earlier ipv6 patch didn't work if no -r option is specified. 2010-12-09 19:20:01 -08:00
95c9b788a0 Add support for IPv6 remote hosts.
Supported sshuttle commands for IPv6:

./sshuttle -r "IPv6:addr" 0.0.0.0/0 -vv
./sshuttle -r "[IPv6:addr]" 0.0.0.0/0 -vv
./sshuttle -r "[IPv6:addr]:22" 0.0.0.0/0 -vv

Technically "invalid" address/port formats, but they can still be parsed because they’re unambiguous, so these also work:

./sshuttle -r "IPv6:addr]" 0.0.0.0/0 -vv
./sshuttle -r "IPv6:addr]:" 0.0.0.0/0 -vv
./sshuttle -r "IPv6:addr]:22" 0.0.0.0/0 -vv
./sshuttle -r "[IPv6:addr" 0.0.0.0/0 -vv

(If you have a Mac with Back To My Mac, use dns-sd to discover the remote host's IPv6 address:
dns-sd -G v4v6 <machine name>.<member name>.members.mac.com )
2010-11-19 15:13:35 -08:00
ef71751846 Add a sshuttle.8 manpage.
You need to have 'pandoc' installed in order to render it from sshuttle.md.
2010-11-09 01:59:51 -08:00
32b4defa9b Add a new --ssh-cmd= option to let you override the ssh command.
Requested by Axel Beckert.
2010-11-09 00:17:01 -08:00
8b7605cc5d Remove the --noserver option.
It didn't work anyway.  Obviously it hasn't been tested (or apparently
needed) in a long time.
2010-11-08 23:59:26 -08:00
bcf1892305 Make password prompting more clear.
Based on suggestions by Jason Grossman and Ed Maste on the mailing list.

We now add a [local su] prefix to the 'su' password prompt (by cheating and
printing it before calling su), and we replace the 'sudo' password prompt
with '[local sudo] Password: ' (by using the little-known and
hopefully-portable -p option).

We no longer call sudo or su if the uid is already 0; otherwise the prefix
on the 'su' prompt would look weird, since su wouldn't ask for a password in
that case.

We don't add a prefix to the ssh password prompt, because it's too hard to
tell if there will *be* an ssh password prompt.  But people will probably
assume that the password request is for the server anyway; few people are
likely to think that 'sshuttle -r myhost.com' is going to prompt for the
*local* password.

Of course none of this is a problem on a modern OS, like Debian, that would
say something like "Password for apenwarr@myhost.com:" instead of just
"Password:".  MacOS doesn't do that, however, so I assume many other OSes
also don't.  Let's try to help them out.
2010-11-08 23:35:16 -08:00
fe742c928d firewall.py: don't die if a given sysctl doesn't exist.
Instead, get a list of known sysctls in the interesting prefix (net.inet.ip)
and check if there's an entry in the list for each sysctl we want to change.
If there isn't, then don't try to change it.

This fixes a problem with FreeBSD, which doesn't have
net.inet.ip.scopedroute but also doesn't need it.  Probably also fixes MacOS
10.5, which probably didn't have that either, but I don't know for sure.

Reported by Ed Maste.
2010-10-16 20:11:30 -06:00
10ce1ee5d4 ipfw: use 'delete' instead of 'del' to avoid a warning on freebsd.
'del' is an abbreviation that happened to work because of substring matching
in earlier versions of ipfw, but apparently they're planning to remove the
substring matching eventually.  In any case, 'delete' has always worked, so
there's no downside to using that.

Reported by Ed Maste.
2010-10-05 13:29:12 -04:00
a32305a275 server.py: don't send partial hostwatch lists.
If hostwatch has a lot of stuff to say all at once, it would come in more
than one recv() packet, and server.py would send each packet individually as
a CMD_HOST_LIST message.  Unfortunately, client.py (rightly) expects each
CMD_HOST_LIST message to be complete, ie. a correct sequence of rows.

So now server.py makes sure of this.  If there's a leftover bit (ie. an
unterminated line), it saves it for later.

Bug reported by user "Duke" on the mailing list.
2010-10-04 02:47:43 -07:00
ae32fe2a59 Merge branch 'python23' - python 2.3 compatibility
* python23:
  Oops, missed another << operator to replace with _shl().
  socket.SHUT_RD and socket.SHUT_WR don't exist in python 2.3.
  compat/ssubprocess.py: some python versions don't have os.closerange().
  _nb_clean: don't catch EPIPE after all.
  Fix busy-waiting in two situations:
  Factor out common mainloop code between client and server.
  Implement our own left-shift operator to shut up python 2.3 warnings.
  Don't use set() since it's not in python 2.3.
  import and use subprocess.py from python 2.6.
  Remove list comprehensions for python 2.3 compatibility.
2010-10-02 16:34:35 -07:00
5070f2ffcf Oops, missed another << operator to replace with _shl().
For python 2.3, of course.
2010-10-02 15:26:29 -07:00
b219b523c2 socket.SHUT_RD and socket.SHUT_WR don't exist in python 2.3.
Mercifully, socket.socket.shutdown() still does, but it uses hardcoded
integer parameters - and the integers correspond to the SHUT_RD and SHUT_WR
definitions in later versions - so let's just hardcode them ourselves.

See the carnage for yourself:
http://docs.python.org/release/2.3.5/lib/socket-objects.html
2010-10-02 15:24:04 -07:00
52fbb2ebbe compat/ssubprocess.py: some python versions don't have os.closerange().
Like python2.5 on Debian.  It might be a MacOS extension or something.  So
much for the comment in subprocess.py that said "keep this compatible with
python 2.2."
2010-10-01 19:26:56 -07:00
76d576a375 _nb_clean: don't catch EPIPE after all.
EPIPE is a serious error from these places, so we have to actually do
something.  Otherwise the client ends up busy waiting when the server
disconnects by surprise.

Bug noticed in a log from Chetan Kunte.
2010-10-01 18:25:03 -07:00
f6e6515a3c Fix busy-waiting in two situations:
- If you tried to connect to a server that didn't exist, then disconnected
  the client during the 60-second connection timeout, the server would
  busy wait for 60 seconds.

- If you connected to a server and then sent data, but then the server
  disconnected before reading all your data, the server would busy wait.
    (example:  yes | telnet servername 80)
2010-10-01 18:22:36 -07:00
84376284db Factor out common mainloop code between client and server.
Also improve the socket message output a bit.
2010-10-01 17:36:09 -07:00
b0f061e204 Implement our own left-shift operator to shut up python 2.3 warnings.
Apparently left-shift in python 2.3 just *always* prints a warning, even if
we weren't doing anything wrong.  Or maybe it only prints the warning
sometimes.  Anyway, let's just multiply by 2**x instead of using <<x, since
we're not performance-sensitive anyway.
2010-10-01 14:46:34 -07:00
c403a83ab8 Don't use set() since it's not in python 2.3.
Just use a plain list instead.  Technically probably slightly worse
asymptotic behaviour, but it's not like we'll have a million sockets anyway.
2010-10-01 14:38:08 -07:00
da774f3f46 import and use subprocess.py from python 2.6.
This should hopefully let us run even on python 2.3 on really old servers.
2010-10-01 12:11:48 -07:00
7d3028dee2 Remove list comprehensions for python 2.3 compatibility. 2010-10-01 11:55:45 -07:00
518df41049 ssh.py: don't os.setsid().
This prevents ssh from asking for a password successfully.  Error reported
by Chetan Kunte.
2010-10-01 11:35:13 -07:00
76bbbfd67b Catch the exception thrown when ssh can't connect.
Easiest test: give it an invalid hostname.

Reported by Chetan Kunte.
2010-10-01 10:34:20 -07:00
6e336c09bf README: remove the note about MacOS not working. It works now! 2010-10-01 00:43:01 -07:00
f950a3800b BSD: sysctl net.inet.ip.forwarding=1 is not necessary.
If your machine is a firewall/router, it affects whether people behind the
router can use your sshuttle connection - in the same way that it affects
whether they can route *anything* through you.  And thus, it should be set
by the admin, not by sshuttle.

sshuttle works fine for the local user either way.

(This also affects MacOS since it's a BSD variant.)
2010-10-01 00:39:30 -07:00
8b4466b802 BSD ipfw: switch from 'established' to 'keep-state/check-state'.
It turns out 'established' doesn't work the way I expected it to from
iptables; it's not stateful.  It just checks the TCP flags to see if the
connection *thinks* it's already established, and follows the rule if so.
That caused the first packet of each new connection to set sent to our
transproxy, but not the subsequent ones, so weird stuff happened.

With this change, any (matching) connection created *after* starting sshuttle
will get forwarded, but pre-existing ones - most importantly, sshuttle's own
ssh connection - will not.

And with this (plus the previous commit), sshuttle works on MacOS, including
10.6!
2010-10-01 00:36:46 -07:00
4bf4f70c67 ssnet: recover slightly more gracefully from an infinite forwarding loop.
If you 'telnet localhost 12300' weird things happen; someday we should
probably auto-detect and avoid that altogether.  But meanwhile, catch EPIPE
if it happens (it's irrelevant) and don't barf with a %d data type for a
value that can apparently sometimes be None.
2010-10-01 00:05:49 -07:00
410b9d4229 Magic incantation to mostly fix MacOS 10.6.
It comes down to this:
   sysctl_set('net.inet.ip.scopedroute', 0)

I say "mostly" because actually it doesn't fix it; sshuttle doesn't know
what to do with the received connection, so there must be a minor bug
remaining somewhere.  I'll fix that next.

Thanks to dkf <dfortunato@gmail.com> on the sshuttle mailing list for
suggesting the magic fix.  He points at this post in particular:
  http://discussions.apple.com/thread.jspa?messageID=11558355&#11558355
that gave him the necessary clue.
2010-10-01 00:05:48 -07:00
2ef1c6a4c4 latest options.py from bup, now with tty-width guessing.
as of bup commit bup-0.19-2-gce2ace5.
2010-09-21 18:03:17 -07:00
b35cfbd022 hostwatch: add missing errno import
If the ~/.sshuttle.hosts file does not exist, it triggers the following
error:

       Traceback (most recent call last):
         File "./sshuttle", line 80, in <module>
           sys.exit(hostwatch.hw_main(extra))
         File "/home/def/p/sshuttle/hostwatch.py", line 246, in hw_main
           read_host_cache()
         File "/home/def/p/sshuttle/hostwatch.py", line 41, in read_host_cache
           if e.errno == errno.ENOENT:
       NameError: global name 'errno' is not defined

(This only happened if you run 'sshuttle --hostwatch' from the command line
directly, without passing it through assembler.py.)
2010-09-21 17:15:46 -07:00
dcba684766 If netstat -rn returns an error, make that non-fatal.
That only really stops --auto-nets from working; it's mostly harmless
otherwise.  And apparently some locked-down shared hosts don't let you get
the list of routes.
2010-09-04 11:29:11 -07:00
ee74110cff add option to allow the remote python binary's name/path to be specified 2010-09-03 23:00:26 -07:00
5bf8687ce3 Import latest options.py from bup-0.17.
This has new support for default values in square brackets, so let's use
that.
2010-09-03 23:00:26 -07:00
6bdb9517fd README: fix some out-of-date system requirements stuff.
Reported by Jason Axelson.
2010-07-25 00:16:09 -04:00
f1b33dab29 Add a --exclude option for excluding subnets from routing.
Also, add 127.0.0.0/8 to the default list of excludes.  If you want to route
0/0, you almost certainly *don't* want to route localhost to the remote ssh
server's localhost!

Thanks to Edward for the suggestion.
2010-07-15 14:13:33 -04:00
3a25f709e5 log(): don't abort if we fail to write to stderr.
Failing to write to the log sucks, but not as much as failing to clean up
just because stderr disappeared.  So let's catch any IOError exception from
log() and just ignore it.

This should fix a problem reported by Camille Moncelier, which is that
sshuttle firewall entries stick around if your tty dies strangely (eg. your
X server aborts for some reason).
2010-05-16 17:57:18 -04:00
a8b3d69856 ssh.py: try harder to find required *.py files.
Search the entire python sys.path, not just the directory that argv[0] is
in.  That way if you symlink the sshuttle binary into (for example) ~/bin,
it'll be able to work correctly.
2010-05-12 13:53:14 -04:00
2d4f6a4308 client: add a debug1() message for connecting/connected.
If the server is going to delay us, we'd at least like to know that.
2010-05-11 19:04:44 -04:00
d435ed837d Created a googlegroups.com mailing list for sshuttle. 2010-05-11 15:30:53 -04:00
2d77403a0b Don't use try/except/finally so that python 2.4 works.
Use try/(try/except)/finally instead.  There was only once case of this.

Thanks to Wayne Scott and nisc for pointing this out.
2010-05-10 13:58:52 -04:00
42 changed files with 4760 additions and 1291 deletions

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
*.pyc *.pyc
*~ *~
*.8
/.do_built
/.do_built.dir
/.redo

11
.travis.yml Normal file
View 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
View File

@ -0,0 +1,5 @@
include *.txt
include *.rst
include *.py
include MANIFEST.in
recursive-include sshuttle *.py

163
README.md
View File

@ -1,163 +0,0 @@
sshuttle: where transparent proxy meets VPN meets ssh
=====================================================
I just spent an afternoon working on a new kind of VPN. You can get
the first release, <a href="http://github.com/apenwarr/sshuttle">sshuttle
0.10, on github</a>.
As far as I know, sshuttle is the only program that solves the following
common case:
- Your client machine (or router) is Linux.
- 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.)
This is how you use it:
-----------------------
- <tt>git clone git://github.com/apenwarr/sshuttle</tt>
on your client and server machines. The server can be
any ssh server with python available; the client must
be Linux with iptables, and you'll need root or sudo
access.
- <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
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>

258
README.rst Normal file
View 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
View File

@ -0,0 +1 @@
0.73

View File

@ -1,26 +0,0 @@
import sys, 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()

219
client.py
View File

@ -1,219 +0,0 @@
import struct, socket, select, subprocess, errno, re
import helpers, ssnet, ssh
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *
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 FirewallClient:
def __init__(self, port, subnets):
self.port = port
self.auto_nets = []
self.subnets = subnets
argvbase = ([sys.argv[0]] +
['-v'] * (helpers.verbose or 0) +
['--firewall', str(port)])
argv_tries = [
['sudo'] + argvbase,
['su', '-c', ' '.join(argvbase)],
argvbase
]
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
# because stupid Linux 'su' requires that stdin be attached to a tty.
# Instead, attach a *bidirectional* socket to its stdout, and use
# that for talking in both directions.
(s1,s2) = socket.socketpair()
def setup():
# run in the child process
s2.close()
e = None
for argv in argv_tries:
try:
self.p = subprocess.Popen(argv, stdout=s1, preexec_fn=setup)
e = None
break
except OSError, e:
pass
self.argv = argv
s1.close()
self.pfile = s2.makefile('wb+')
if e:
log('Spawning firewall manager: %r\n' % self.argv)
raise Fatal(e)
line = self.pfile.readline()
self.check()
if line != 'READY\n':
raise Fatal('%r expected READY, got %r' % (self.argv, line))
def check(self):
rv = self.p.poll()
if rv:
raise Fatal('%r returned %d' % (self.argv, rv))
def start(self):
self.pfile.write('ROUTES\n')
for (ip,width) in self.subnets+self.auto_nets:
self.pfile.write('%s,%d\n' % (ip, width))
self.pfile.write('GO\n')
self.pfile.flush()
line = self.pfile.readline()
self.check()
if line != 'STARTED\n':
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip):
assert(not re.search(r'[^-\w]', hostname))
assert(not re.search(r'[^0-9.]', ip))
self.pfile.write('HOST %s,%s\n' % (hostname, ip))
self.pfile.flush()
def done(self):
self.pfile.close()
rv = self.p.wait()
if rv:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
def _main(listener, fw, use_server, remotename, seed_hosts, auto_nets):
handlers = []
if use_server:
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
(serverproc, serversock) = ssh.connect(remotename)
mux = Mux(serversock, serversock)
handlers.append(mux)
expected = 'SSHUTTLE0001'
initstring = serversock.recv(len(expected))
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split('\n'):
(ip,width) = line.split(',', 1)
fw.auto_nets.append((ip,int(width)))
# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
#
# Moreover, now that we have the --auto-nets option, we have to wait
# for the server to send us that message anyway. Even if we haven't
# set --auto-nets, we might as well wait for the message first, then
# ignore its contents.
mux.got_routes = None
fw.start()
mux.got_routes = onroutes
def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist)
for line in hostlist.strip().split():
if line:
name,ip = line.split(',', 1)
fw.sethostip(name, ip)
mux.got_host_list = onhostlist
def onaccept():
sock,srcip = listener.accept()
dstip = original_dst(sock)
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
dstip[0],dstip[1]))
if dstip == listener.getsockname():
debug1("-- ignored: that's my address!\n")
sock.close()
return
if use_server:
chan = mux.next_channel()
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
outwrap = MuxWrapper(mux, chan)
else:
outwrap = ssnet.connect_dst(dstip[0], dstip[1])
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
handlers.append(Handler([listener], onaccept))
if seed_hosts != None:
debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
while 1:
if use_server:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
r = set()
w = set()
x = set()
handlers = filter(lambda s: s.ok, handlers)
for s in handlers:
s.pre_select(r,w,x)
debug2('Waiting: %d[%d,%d,%d]...\n'
% (len(handlers), len(r), len(w), len(x)))
(r,w,x) = select.select(r,w,x)
#log('r=%r w=%r x=%r\n' % (r,w,x))
ready = set(r) | set(w) | set(x)
for s in handlers:
if s.socks & ready:
s.callback()
if use_server:
mux.callback()
mux.check_fullness()
def main(listenip, use_server, remotename, seed_hosts, auto_nets, subnets):
debug1('Starting sshuttle proxy.\n')
listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if listenip[1]:
ports = [listenip[1]]
else:
ports = xrange(12300,9000,-1)
last_e = None
bound = False
debug2('Binding:')
for port in ports:
debug2(' %d' % port)
try:
listener.bind((listenip[0], port))
bound = True
break
except socket.error, e:
last_e = e
debug2('\n')
if not bound:
assert(last_e)
raise last_e
listener.listen(10)
listenip = listener.getsockname()
debug1('Listening on %r.\n' % (listenip,))
fw = FirewallClient(listenip[1], subnets)
try:
return _main(listener, fw, use_server, remotename,
seed_hosts, auto_nets)
finally:
fw.done()

View File

@ -1,268 +0,0 @@
import subprocess, re, errno
import helpers
from helpers import *
def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
p = subprocess.Popen(argv, stdout = subprocess.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(*args):
argv = ['iptables', '-t', 'nat'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
# 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(port, subnets):
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
if ipt_chain_exists(chain):
ipt('-D', 'OUTPUT', '-j', chain)
ipt('-D', 'PREROUTING', '-j', chain)
ipt('-F', chain)
ipt('-X', chain)
if subnets:
ipt('-N', chain)
ipt('-F', chain)
ipt('-I', 'OUTPUT', '1', '-j', chain)
ipt('-I', 'PREROUTING', '1', '-j', chain)
# create new subnet entries
for snet,swidth in subnets:
ipt('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp',
'--to-ports', str(port),
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
)
def ipfw_rule_exists(n):
argv = ['ipfw', 'list']
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout:
if line.startswith('%05d ' % n):
if line.find('ipttl 42') < 0 and line.find('established') < 0:
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
return True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def sysctl_get(name):
argv = ['sysctl', '-n', name]
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
line = p.stdout.readline()
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
if not line:
raise Fatal('%r returned no data' % (argv,))
assert(line[-1] == '\n')
return line[:-1]
def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv, stdout = open('/dev/null', 'w'))
_oldctls = []
def sysctl_set(name, val):
oldval = sysctl_get(name)
if str(val) != str(oldval):
_oldctls.append((name, oldval))
return _sysctl_set(name, val)
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, subnets):
sport = str(port)
# cleanup any existing rules
if ipfw_rule_exists(port):
ipfw('del', sport)
while _oldctls:
(name,oldval) = _oldctls.pop()
_sysctl_set(name, oldval)
if subnets:
sysctl_set('net.inet.ip.fw.enable', 1)
sysctl_set('net.inet.ip.forwarding', 1)
ipfw('add', sport, 'accept', 'ip',
'from', 'any', 'to', 'any', 'established')
# create new subnet entries
for snet,swidth in subnets:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
'log', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth),
'not', 'ipttl', '42')
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 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):
assert(port > 0)
assert(port <= 65535)
if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
if program_exists('ipfw'):
do_it = do_ipfw
elif program_exists('iptables'):
do_it = do_iptables
else:
raise Fatal("can't find either ipfw or iptables; check your PATH")
# 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)
debug1('firewall manager ready.\n')
sys.stdout.write('READY\n')
sys.stdout.flush()
# 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:
(ip,width) = line.strip().split(',', 1)
except:
raise Fatal('firewall: expected route or GO but got %r' % line)
subnets.append((ip, int(width)))
try:
if line:
debug1('firewall manager: starting transproxy.\n')
do_it(port, subnets)
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:
line = sys.stdin.readline(128)
if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip
rewrite_etc_hosts(port)
elif line:
raise Fatal('expected EOF, got %r' % line)
else:
break
finally:
try:
debug1('firewall manager: undoing changes.\n')
except:
pass
do_it(port, [])
restore_etc_hosts(port)

View File

@ -1,25 +0,0 @@
import sys, os
logprefix = ''
verbose = 0
def log(s):
sys.stdout.flush()
sys.stderr.write(logprefix + s)
sys.stderr.flush()
def debug1(s):
if verbose >= 1:
log(s)
def debug2(s):
if verbose >= 2:
log(s)
def debug3(s):
if verbose >= 3:
log(s)
class Fatal(Exception):
pass

105
main.py
View File

@ -1,105 +0,0 @@
#!/usr/bin/env python
import sys, os, re
import helpers, options, client, server, firewall, hostwatch
from helpers import *
# list of:
# 1.2.3.4/5 or just 1.2.3.4
def parse_subnets(subnets_str):
subnets = []
for s in subnets_str:
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP subnet format' % s)
(a,b,c,d,width) = m.groups()
(a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
if width == None:
width = 32
else:
width = int(width)
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
if width > 32:
raise Fatal('*/%d is greater than the maximum of 32' % width)
subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width))
return subnets
# 1.2.3.4:567 or just 1.2.3.4 or just 567
def parse_ipport(s):
s = str(s)
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP:port format' % s)
(a,b,c,d,port) = m.groups()
(a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
int(port or 0))
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d))
if port > 65535:
raise Fatal('*:%d is greater than the maximum of 65535' % port)
if a == None:
a = b = c = d = 0
return ('%d.%d.%d.%d' % (a,b,c,d), port)
optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --firewall <port> <subnets...>
sshuttle --server
--
l,listen= transproxy to this ip address and port number [default=0]
H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route
r,remote= ssh hostname (and optional username) of remote sshuttle server
v,verbose increase debug message verbosity
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
noserver don't use a separate server process (mostly for debugging)
server [internal use only]
firewall [internal use only]
hostwatch [internal use only]
"""
o = options.Options('sshuttle', optspec)
(opt, flags, extra) = o.parse(sys.argv[1:])
helpers.verbose = opt.verbose
try:
if opt.server:
if len(extra) != 0:
o.fatal('no arguments expected')
sys.exit(server.main())
elif opt.firewall:
if len(extra) != 1:
o.fatal('exactly one argument expected')
sys.exit(firewall.main(int(extra[0])))
elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra))
else:
if len(extra) < 1 and not opt.auto_nets:
o.fatal('at least one subnet (or -N) expected')
remotename = opt.remote
if remotename == '' or remotename == '-':
remotename = None
if opt.seed_hosts and not opt.auto_hosts:
o.fatal('--seed-hosts only works if you also use -H')
if opt.seed_hosts:
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
elif opt.auto_hosts:
sh = []
else:
sh = None
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
not opt.noserver,
remotename,
sh,
opt.auto_nets,
parse_subnets(extra)))
except Fatal, e:
log('fatal: %s\n' % e)
sys.exit(99)
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
sys.exit(1)

View File

@ -1,118 +0,0 @@
import sys, textwrap, getopt, re
class OptDict:
def __init__(self):
self._opts = {}
def __setitem__(self, k, v):
self._opts[k] = v
def __getitem__(self, k):
return self._opts[k]
def __getattr__(self, k):
return self[k]
class Options:
def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt):
self.exe = exe
self.optspec = optspec
self.optfunc = optfunc
self._aliases = {}
self._shortopts = 'h?'
self._longopts = ['help']
self._hasparms = {}
self._usagestr = self._gen_usage()
def _gen_usage(self):
out = []
lines = self.optspec.strip().split('\n')
lines.reverse()
first_syn = True
while lines:
l = lines.pop()
if l == '--': break
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
first_syn = False
out.append('\n')
while lines:
l = lines.pop()
if l.startswith(' '):
out.append('\n%s\n' % l.lstrip())
elif l:
(flags, extra) = l.split(' ', 1)
extra = extra.strip()
if flags.endswith('='):
flags = flags[:-1]
has_parm = 1
else:
has_parm = 0
flagl = flags.split(',')
flagl_nice = []
for f in flagl:
f_nice = re.sub(r'\W', '_', f)
self._aliases[f] = flagl[0]
self._aliases[f_nice] = flagl[0]
self._hasparms[f] = has_parm
if len(f) == 1:
self._shortopts += f + (has_parm and ':' or '')
flagl_nice.append('-' + f)
else:
assert(not f.startswith('no-')) # supported implicitly
self._longopts.append(f + (has_parm and '=' or ''))
self._longopts.append('no-' + f)
flagl_nice.append('--' + f)
flags_nice = ', '.join(flagl_nice)
if has_parm:
flags_nice += ' ...'
prefix = ' %-20s ' % flags_nice
argtext = '\n'.join(textwrap.wrap(extra, width=70,
initial_indent=prefix,
subsequent_indent=' '*28))
out.append(argtext + '\n')
else:
out.append('\n')
return ''.join(out).rstrip() + '\n'
def usage(self):
sys.stderr.write(self._usagestr)
sys.exit(97)
def fatal(self, s):
sys.stderr.write('error: %s\n' % s)
return self.usage()
def parse(self, args):
try:
(flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
except getopt.GetoptError, e:
self.fatal(e)
opt = OptDict()
for f in self._aliases.values():
opt[f] = None
for (k,v) in flags:
while k.startswith('-'):
k = k[1:]
if k in ['h', '?', 'help']:
self.usage()
if k.startswith('no-'):
k = self._aliases[k[3:]]
opt[k] = None
else:
k = self._aliases[k]
if not self._hasparms[k]:
assert(v == '')
opt[k] = (opt._opts.get(k) or 0) + 1
else:
try:
vv = int(v)
if str(vv) == v:
v = vv
except ValueError:
pass
opt[k] = v
for (f1,f2) in self._aliases.items():
opt[f1] = opt[f2]
return (opt,flags,extra)

6
run Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
if python2 -V 2>/dev/null; then
exec python2 -m "sshuttle" "$@"
else
exec python -m "sshuttle" "$@"
fi

174
server.py
View File

@ -1,174 +0,0 @@
import re, struct, socket, select, subprocess, traceback
if not globals().get('skip_imports'):
import ssnet, helpers, hostwatch
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *
def _ipmatch(ipstr):
if ipstr == 'default':
ipstr = '0.0.0.0/0'
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m:
g = m.groups()
ips = g[0]
width = int(g[4] or 32)
if g[1] == None:
ips += '.0.0.0'
width = min(width, 8)
elif g[2] == None:
ips += '.0.0'
width = min(width, 16)
elif g[3] == None:
ips += '.0'
width = min(width, 24)
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
def _ipstr(ip, width):
if width >= 32:
return ip
else:
return "%s/%d" % (ip, width)
def _maskbits(netmask):
if not netmask:
return 32
for i in range(32):
if netmask[0] & (1<<i):
return 32-i
return 0
def _list_routes():
argv = ['netstat', '-rn']
p = subprocess.Popen(argv, stdout=subprocess.PIPE)
routes = []
for line in p.stdout:
cols = re.split(r'\s+', line)
ipw = _ipmatch(cols[0])
if not ipw:
continue # some lines won't be parseable; never mind
maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null
width = min(ipw[1], mask)
ip = ipw[0] & (((1<<width)-1) << (32-width))
routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait()
if rv != 0:
raise Fatal('%r returned %d' % (argv, rv))
return routes
def list_routes():
for (ip,width) in _list_routes():
if not ip.startswith('0.') and not ip.startswith('127.'):
yield (ip,width)
def _exc_dump():
exc_info = sys.exc_info()
return ''.join(traceback.format_exception(*exc_info))
def start_hostwatch(seed_hosts):
s1,s2 = socket.socketpair()
pid = os.fork()
if not pid:
# child
rv = 99
try:
s2.close()
os.dup2(s1.fileno(), 1)
os.dup2(s1.fileno(), 0)
s1.close()
rv = hostwatch.hw_main(seed_hosts) or 0
except Exception, e:
log('%s\n' % _exc_dump())
rv = 98
finally:
os._exit(rv)
s1.close()
return pid,s2
class Hostwatch:
def __init__(self):
self.pid = 0
self.sock = None
def main():
if helpers.verbose >= 1:
helpers.logprefix = ' s: '
else:
helpers.logprefix = 'server: '
routes = list(list_routes())
debug1('available routes:\n')
for r in routes:
debug1(' %s/%d\n' % r)
# synchronization header
sys.stdout.write('SSHUTTLE0001')
sys.stdout.flush()
handlers = []
mux = Mux(socket.fromfd(sys.stdin.fileno(),
socket.AF_INET, socket.SOCK_STREAM),
socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM))
handlers.append(mux)
routepkt = ''.join('%s,%d\n' % r
for r in routes)
mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch()
def hostwatch_ready():
assert(hw.pid)
content = hw.sock.recv(4096)
if content:
mux.send(0, ssnet.CMD_HOST_LIST, content)
else:
raise Fatal('hostwatch process died')
def got_host_req(data):
if not hw.pid:
(hw.pid,hw.sock) = start_hostwatch(data.strip().split())
handlers.append(Handler(socks = [hw.sock],
callback = hostwatch_ready))
mux.got_host_req = got_host_req
def new_channel(channel, data):
(dstip,dstport) = data.split(',', 1)
dstport = int(dstport)
outwrap = ssnet.connect_dst(dstip,dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
while mux.ok:
if hw.pid:
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
r = set()
w = set()
x = set()
handlers = filter(lambda s: s.ok, handlers)
for s in handlers:
s.pre_select(r,w,x)
debug2('Waiting: %d[%d,%d,%d] (fullness=%d/%d)...\n'
% (len(handlers), len(r), len(w), len(x),
mux.fullness, mux.too_full))
(r,w,x) = select.select(r,w,x)
#log('r=%r w=%r x=%r\n' % (r,w,x))
ready = set(r) | set(w) | set(x)
for s in handlers:
#debug2('check: %r: %r\n' % (s, s.socks & ready))
if s.socks & ready:
s.callback()
mux.check_fullness()
mux.callback()

52
setup.py Executable file
View 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",
)

64
ssh.py
View File

@ -1,64 +0,0 @@
import sys, os, re, subprocess, socket, zlib
import helpers
from helpers import *
def readfile(name):
basedir = os.path.dirname(os.path.abspath(sys.argv[0]))
fullname = os.path.join(basedir, name)
return open(fullname, 'rb').read()
def empackage(z, filename):
content = z.compress(readfile(filename))
content += z.flush(zlib.Z_SYNC_FLUSH)
return '%s\n%d\n%s' % (filename,len(content), content)
def connect(rhostport):
main_exe = sys.argv[0]
l = (rhostport or '').split(':', 1)
rhost = l[0]
portl = []
if len(l) > 1:
portl = ['-p', str(int(l[1]))]
if rhost == '-':
rhost = None
z = zlib.compressobj(1)
content = readfile('assembler.py')
content2 = (empackage(z, 'helpers.py') +
empackage(z, 'ssnet.py') +
empackage(z, 'hostwatch.py') +
empackage(z, 'server.py') +
"\n")
pyscript = r"""
import sys;
skip_imports=1;
verbosity=%d;
exec compile(sys.stdin.read(%d), "assembler.py", "exec")
""" % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
if not rhost:
argv = ['python', '-c', pyscript]
else:
argv = ['ssh'] + portl + [rhost, '--', "python -c '%s'" % pyscript]
(s1,s2) = socket.socketpair()
def setup():
# runs in the child process
s2.close()
os.setsid()
s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close()
debug2('executing: %r\n' % argv)
p = subprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True)
os.close(s1a)
os.close(s1b)
s2.sendall(content)
s2.sendall(content2)
return p, s2

View File

@ -1 +0,0 @@
main.py

0
sshuttle/__init__.py Normal file
View File

226
sshuttle/__main__.py Normal file
View File

@ -0,0 +1,226 @@
import sys
import re
import socket
import sshuttle.helpers as helpers
import sshuttle.options as options
import sshuttle.client as client
import sshuttle.firewall as firewall
import sshuttle.hostwatch as hostwatch
from sshuttle.helpers import family_ip_tuple, log, Fatal
# 1.2.3.4/5 or just 1.2.3.4
def parse_subnet4(s):
m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP subnet format' % s)
(a, b, c, d, width) = m.groups()
(a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
if width is None:
width = 32
else:
width = int(width)
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
if width > 32:
raise Fatal('*/%d is greater than the maximum of 32' % width)
return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width)
# 1:2::3/64 or just 1:2::3
def parse_subnet6(s):
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP subnet format' % s)
(net, width) = m.groups()
if width is None:
width = 128
else:
width = int(width)
if width > 128:
raise Fatal('*/%d is greater than the maximum of 128' % width)
return(socket.AF_INET6, net, width)
# Subnet file, supporting empty lines and hash-started comment lines
def parse_subnet_file(s):
try:
handle = open(s, 'r')
except OSError:
raise Fatal('Unable to open subnet file: %s' % s)
raw_config_lines = handle.readlines()
config_lines = []
for line_no, line in enumerate(raw_config_lines):
line = line.strip()
if len(line) == 0:
continue
if line[0] == '#':
continue
config_lines.append(line)
return config_lines
# list of:
# 1.2.3.4/5 or just 1.2.3.4
# 1:2::3/64 or just 1:2::3
def parse_subnets(subnets_str):
subnets = []
for s in subnets_str:
if ':' in s:
subnet = parse_subnet6(s)
else:
subnet = parse_subnet4(s)
subnets.append(subnet)
return subnets
# 1.2.3.4:567 or just 1.2.3.4 or just 567
def parse_ipport4(s):
s = str(s)
m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP:port format' % s)
(a, b, c, d, port) = m.groups()
(a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
int(port or 0))
if a > 255 or b > 255 or c > 255 or d > 255:
raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
if port > 65535:
raise Fatal('*:%d is greater than the maximum of 65535' % port)
if a is None:
a = b = c = d = 0
return ('%d.%d.%d.%d' % (a, b, c, d), port)
# [1:2::3]:456 or [1:2::3] or 456
def parse_ipport6(s):
s = str(s)
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%s is not a valid IP:port format' % s)
(ip, port) = m.groups()
(ip, port) = (ip or '::', int(port or 0))
return (ip, port)
def parse_list(list):
return re.split(r'[\s,]+', list.strip()) if list else []
optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --firewall <port> <subnets...>
sshuttle --hostwatch
--
l,listen= transproxy to this ip address and port number
H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route
dns capture local DNS requests and forward to the remote DNS server
ns-hosts= capture and forward remote DNS requests to the following servers
method= auto, nat, tproxy, pf or ipfw
python= path to python interpreter on the remote server
r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once)
X,exclude-from= exclude the subnets in a file (whitespace separated)
v,verbose increase debug message verbosity
e,ssh-cmd= the command to use to connect to the remote [ssh]
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
no-latency-control sacrifice latency to improve bandwidth benchmarks
wrap= restart counting channel numbers after this number (for testing)
D,daemon run in the background as a daemon
s,subnets= file where the subnets are stored, instead of on the command line
syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
server (internal use only)
firewall (internal use only)
hostwatch (internal use only)
"""
o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[1:])
if opt.daemon:
opt.syslog = 1
if opt.wrap:
import sshuttle.ssnet as ssnet
ssnet.MAX_CHANNEL = int(opt.wrap)
helpers.verbose = opt.verbose
try:
if opt.firewall:
if len(extra) != 0:
o.fatal('exactly zero arguments expected')
result = firewall.main(opt.method, opt.syslog)
sys.exit(result)
elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra))
else:
if len(extra) < 1 and not opt.auto_nets and not opt.subnets:
o.fatal('at least one subnet, subnet file, or -N expected')
includes = extra
excludes = ['127.0.0.0/8']
for k, v in flags:
if k in ('-x', '--exclude'):
excludes.append(v)
if k in ('-X', '--exclude-from'):
excludes += open(v).read().split()
remotename = opt.remote
if remotename == '' or remotename == '-':
remotename = None
nslist = [family_ip_tuple(ns) for ns in parse_list(opt.ns_hosts)]
if opt.seed_hosts and not opt.auto_hosts:
o.fatal('--seed-hosts only works if you also use -H')
if opt.seed_hosts:
sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip())
elif opt.auto_hosts:
sh = []
else:
sh = None
if opt.subnets:
includes = parse_subnet_file(opt.subnets)
if not opt.method:
method_name = "auto"
elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]:
method_name = opt.method
else:
o.fatal("method_name %s not supported" % opt.method)
if not opt.listen:
ipport_v6 = "auto" # parse_ipport6('[::1]:0')
ipport_v4 = "auto" # parse_ipport4('127.0.0.1:0')
else:
ipport_v6 = None
ipport_v4 = None
list = opt.listen.split(",")
for ip in list:
if '[' in ip and ']' in ip:
ipport_v6 = parse_ipport6(ip)
else:
ipport_v4 = parse_ipport4(ip)
return_code = client.main(ipport_v6, ipport_v4,
opt.ssh_cmd,
remotename,
opt.python,
opt.latency_control,
opt.dns,
nslist,
method_name,
sh,
opt.auto_nets,
parse_subnets(includes),
parse_subnets(excludes),
opt.syslog, opt.daemon, opt.pidfile)
if return_code == 0:
log('Normal exit code, exiting...')
else:
log('Abnormal exit code detected, failing...' % return_code)
sys.exit(return_code)
except Fatal as e:
log('fatal: %s\n' % e)
sys.exit(99)
except KeyboardInterrupt:
log('\n')
log('Keyboard interrupt: exiting.\n')
sys.exit(1)

34
sshuttle/assembler.py Normal file
View 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)

653
sshuttle/client.py Normal file
View File

@ -0,0 +1,653 @@
import socket
import errno
import re
import signal
import time
import subprocess as ssubprocess
import sshuttle.helpers as helpers
import os
import sshuttle.ssnet as ssnet
import sshuttle.ssh as ssh
import sshuttle.ssyslog as ssyslog
import sys
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
resolvconf_nameservers
from sshuttle.methods import get_method
_extra_fd = os.open('/dev/null', os.O_RDONLY)
def got_signal(signum, frame):
log('exiting on signal %d\n' % signum)
sys.exit(1)
_pidname = None
def check_daemon(pidfile):
global _pidname
_pidname = os.path.abspath(pidfile)
try:
oldpid = open(_pidname).read(1024)
except IOError as e:
if e.errno == errno.ENOENT:
return # no pidfile, ok
else:
raise Fatal("can't read %s: %s" % (_pidname, e))
if not oldpid:
os.unlink(_pidname)
return # invalid pidfile, ok
oldpid = int(oldpid.strip() or 0)
if oldpid <= 0:
os.unlink(_pidname)
return # invalid pidfile, ok
try:
os.kill(oldpid, 0)
except OSError as e:
if e.errno == errno.ESRCH:
os.unlink(_pidname)
return # outdated pidfile, ok
elif e.errno == errno.EPERM:
pass
else:
raise
raise Fatal("%s: sshuttle is already running (pid=%d)"
% (_pidname, oldpid))
def daemonize():
if os.fork():
os._exit(0)
os.setsid()
if os.fork():
os._exit(0)
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
try:
os.write(outfd, '%d\n' % os.getpid())
finally:
os.close(outfd)
os.chdir("/")
# Normal exit when killed, or try/finally won't work and the pidfile won't
# be deleted.
signal.signal(signal.SIGTERM, got_signal)
si = open('/dev/null', 'r+')
os.dup2(si.fileno(), 0)
os.dup2(si.fileno(), 1)
si.close()
ssyslog.stderr_to_syslog()
def daemon_cleanup():
try:
os.unlink(_pidname)
except OSError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
class MultiListener:
def __init__(self, type=socket.SOCK_STREAM, proto=0):
self.v6 = socket.socket(socket.AF_INET6, type, proto)
self.v4 = socket.socket(socket.AF_INET, type, proto)
def setsockopt(self, level, optname, value):
if self.v6:
self.v6.setsockopt(level, optname, value)
if self.v4:
self.v4.setsockopt(level, optname, value)
def add_handler(self, handlers, callback, method, mux):
socks = []
if self.v6:
socks.append(self.v6)
if self.v4:
socks.append(self.v4)
handlers.append(
Handler(
socks,
lambda sock: callback(sock, method, mux, handlers)
)
)
def listen(self, backlog):
if self.v6:
self.v6.listen(backlog)
if self.v4:
try:
self.v4.listen(backlog)
except socket.error as e:
# on some systems v4 bind will fail if the v6 suceeded,
# in this case the v6 socket will receive v4 too.
if e.errno == errno.EADDRINUSE and self.v6:
self.v4 = None
else:
raise e
def bind(self, address_v6, address_v4):
if address_v6 and self.v6:
self.v6.bind(address_v6)
else:
self.v6 = None
if address_v4 and self.v4:
self.v4.bind(address_v4)
else:
self.v4 = None
def print_listening(self, what):
if self.v6:
listenip = self.v6.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v6))
if self.v4:
listenip = self.v4.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
debug2('%s listening with %r.\n' % (what, self.v4))
class FirewallClient:
def __init__(self, method_name):
self.auto_nets = []
python_path = os.path.dirname(os.path.dirname(__file__))
argvbase = ([sys.executable, sys.argv[0]] +
['-v'] * (helpers.verbose or 0) +
['--method', method_name] +
['--firewall'])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [
['sudo', '-p', '[local sudo] Password: ',
('PYTHONPATH=%s' % python_path), '--'] + argvbase,
argvbase
]
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
# because stupid Linux 'su' requires that stdin be attached to a tty.
# Instead, attach a *bidirectional* socket to its stdout, and use
# that for talking in both directions.
(s1, s2) = socket.socketpair()
def setup():
# run in the child process
s2.close()
e = None
if os.getuid() == 0:
argv_tries = argv_tries[-1:] # last entry only
for argv in argv_tries:
try:
if argv[0] == 'su':
sys.stderr.write('[local su] ')
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
e = None
break
except OSError as e:
pass
self.argv = argv
s1.close()
if sys.version_info < (3, 0):
# python 2.7
self.pfile = s2.makefile('wb+')
else:
# python 3.5
self.pfile = s2.makefile('rwb')
if e:
log('Spawning firewall manager: %r\n' % self.argv)
raise Fatal(e)
line = self.pfile.readline()
self.check()
if line[0:5] != b'READY':
raise Fatal('%r expected READY, got %r' % (self.argv, line))
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):
rv = self.p.poll()
if rv:
raise Fatal('%r returned %d' % (self.argv, rv))
def start(self):
self.pfile.write(b'ROUTES\n')
for (family, ip, width) in self.subnets_include + self.auto_nets:
self.pfile.write(b'%d,%d,0,%s\n'
% (family, width, ip.encode("ASCII")))
for (family, ip, width) in self.subnets_exclude:
self.pfile.write(b'%d,%d,1,%s\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()
line = self.pfile.readline()
self.check()
if line != b'STARTED\n':
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip):
assert(not re.search(r'[^-\w]', hostname))
assert(not re.search(r'[^0-9.]', ip))
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
self.pfile.flush()
def done(self):
self.pfile.close()
rv = self.p.wait()
if rv:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
dnsreqs = {}
udp_by_src = {}
def expire_connections(now, mux):
for chan, timeout in dnsreqs.items():
if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan)
del mux.channels[chan]
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
for peer, (chan, timeout) in udp_by_src.items():
if timeout < now:
debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
mux.send(chan, ssnet.CMD_UDP_CLOSE, '')
del mux.channels[chan]
del udp_by_src[peer]
debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
def onaccept_tcp(listener, method, mux, handlers):
global _extra_fd
try:
sock, srcip = listener.accept()
except socket.error as e:
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
debug1('Rejected incoming connection: too many open files!\n')
# free up an fd so we can eat the connection
os.close(_extra_fd)
try:
sock, srcip = listener.accept()
sock.close()
finally:
_extra_fd = os.open('/dev/null', os.O_RDONLY)
return
else:
raise
dstip = method.get_tcp_dstip(sock)
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
dstip[0], dstip[1]))
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
debug1("-- ignored: that's my address!\n")
sock.close()
return
chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
sock.close()
return
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
(sock.family, dstip[0].encode("ASCII"), dstip[1]))
outwrap = MuxWrapper(mux, chan)
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
expire_connections(time.time(), mux)
def udp_done(chan, data, method, sock, dstip):
(src, srcport, data) = data.split(",", 2)
srcip = (src, int(srcport))
debug3('doing send from %r to %r\n' % (srcip, dstip,))
method.send_udp(sock, srcip, dstip, data)
def onaccept_udp(listener, method, mux, handlers):
now = time.time()
t = method.recv_udp(listener, 4096)
if t is None:
return
srcip, dstip, data = t
debug1('Accept UDP: %r -> %r.\n' % (srcip, dstip,))
if srcip in udp_by_src:
chan, timeout = udp_by_src[srcip]
else:
chan = mux.next_channel()
mux.channels[chan] = lambda cmd, data: udp_done(
chan, data, method, listener, dstip=srcip)
mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family)
udp_by_src[srcip] = chan, now + 30
hdr = "%s,%r," % (dstip[0], dstip[1])
mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data)
expire_connections(now, mux)
def dns_done(chan, data, method, sock, srcip, dstip, mux):
debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan, srcip, dstip))
del mux.channels[chan]
del dnsreqs[chan]
method.send_udp(sock, srcip, dstip, data)
def ondns(listener, method, mux, handlers):
now = time.time()
t = method.recv_udp(listener, 4096)
if t is None:
return
srcip, dstip, data = t
debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data)))
chan = mux.next_channel()
dnsreqs[chan] = now + 30
mux.send(chan, ssnet.CMD_DNS_REQ, data)
mux.channels[chan] = lambda cmd, data: dns_done(
chan, data, method, listener, srcip=dstip, dstip=srcip, mux=mux)
expire_connections(now, mux)
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control,
dns_listener, seed_hosts, auto_nets,
syslog, daemon):
method = fw.method
handlers = []
if helpers.verbose >= 1:
helpers.logprefix = 'c : '
else:
helpers.logprefix = 'client: '
debug1('connecting to server...\n')
try:
(serverproc, serversock) = ssh.connect(
ssh_cmd, remotename, python,
stderr=ssyslog._p and ssyslog._p.stdin,
options=dict(latency_control=latency_control))
except socket.error as e:
if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)")
else:
raise
mux = Mux(serversock, serversock)
handlers.append(mux)
expected = b'SSHUTTLE0001'
try:
v = 'x'
while v and v != b'\0':
v = serversock.recv(1)
v = 'x'
while v and v != b'\0':
v = serversock.recv(1)
initstring = serversock.recv(len(expected))
except socket.error as e:
if e.args[0] == errno.ECONNRESET:
raise Fatal("failed to establish ssh session (2)")
else:
raise
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
debug1('connected.\n')
print('Connected.')
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
elif syslog:
debug1('switching to syslog.\n')
ssyslog.stderr_to_syslog()
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split('\n'):
(family, ip, width) = line.split(',', 2)
fw.auto_nets.append((int(family), ip, int(width)))
# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
#
# Moreover, now that we have the --auto-nets option, we have to wait
# for the server to send us that message anyway. Even if we haven't
# set --auto-nets, we might as well wait for the message first, then
# ignore its contents.
mux.got_routes = None
fw.start()
mux.got_routes = onroutes
def onhostlist(hostlist):
debug2('got host list: %r\n' % hostlist)
for line in hostlist.strip().split():
if line:
name, ip = line.split(',', 1)
fw.sethostip(name, ip)
mux.got_host_list = onhostlist
tcp_listener.add_handler(handlers, onaccept_tcp, method, mux)
if udp_listener:
udp_listener.add_handler(handlers, onaccept_udp, method, mux)
if dns_listener:
dns_listener.add_handler(handlers, ondns, method, mux)
if seed_hosts is not None:
debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
while 1:
rv = serverproc.poll()
if rv:
raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, nslist,
method_name, seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog:
ssyslog.start_syslog()
if daemon:
try:
check_daemon(pidfile)
except Fatal as e:
log("%s\n" % e)
return 5
debug1('Starting sshuttle proxy.\n')
fw = FirewallClient(method_name)
features = fw.method.get_supported_features()
if listenip_v6 == "auto":
if features.ipv6:
listenip_v6 = ('::1', 0)
else:
listenip_v6 = None
if listenip_v4 == "auto":
listenip_v4 = ('127.0.0.1', 0)
udp = features.udp
debug1("UDP enabled: %r\n" % udp)
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
# if both ports given, no need to search for a spare port
ports = [0, ]
else:
# if at least one port missing, we have to search
ports = range(12300, 9000, -1)
# search for free ports and try to bind
last_e = None
redirectport_v6 = 0
redirectport_v4 = 0
bound = False
debug2('Binding redirector:')
for port in ports:
debug2(' %d' % port)
tcp_listener = MultiListener()
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if udp:
udp_listener = MultiListener(socket.SOCK_DGRAM)
udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
else:
udp_listener = None
if listenip_v6 and listenip_v6[1]:
lv6 = listenip_v6
redirectport_v6 = lv6[1]
elif listenip_v6:
lv6 = (listenip_v6[0], port)
redirectport_v6 = port
else:
lv6 = None
redirectport_v6 = 0
if listenip_v4 and listenip_v4[1]:
lv4 = listenip_v4
redirectport_v4 = lv4[1]
elif listenip_v4:
lv4 = (listenip_v4[0], port)
redirectport_v4 = port
else:
lv4 = None
redirectport_v4 = 0
try:
tcp_listener.bind(lv6, lv4)
if udp_listener:
udp_listener.bind(lv6, lv4)
bound = True
break
except socket.error as e:
if e.errno == errno.EADDRINUSE:
last_e = e
else:
raise e
debug2('\n')
if not bound:
assert(last_e)
raise last_e
tcp_listener.listen(10)
tcp_listener.print_listening("TCP redirector")
if udp_listener:
udp_listener.print_listening("UDP redirector")
bound = False
if dns or nslist:
if dns:
nslist += resolvconf_nameservers()
dns = True
# search for spare port for DNS
debug2('Binding DNS:')
ports = range(12300, 9000, -1)
for port in ports:
debug2(' %d' % port)
dns_listener = MultiListener(socket.SOCK_DGRAM)
if listenip_v6:
lv6 = (listenip_v6[0], port)
dnsport_v6 = port
else:
lv6 = None
dnsport_v6 = 0
if listenip_v4:
lv4 = (listenip_v4[0], port)
dnsport_v4 = port
else:
lv4 = None
dnsport_v4 = 0
try:
dns_listener.bind(lv6, lv4)
bound = True
break
except socket.error as e:
if e.errno == errno.EADDRINUSE:
last_e = e
else:
raise e
debug2('\n')
dns_listener.print_listening("DNS")
if not bound:
assert(last_e)
raise last_e
else:
dnsport_v6 = 0
dnsport_v4 = 0
dns_listener = None
fw.method.check_settings(udp, dns)
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)
fw.setup(subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
udp)
try:
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, dns_listener,
seed_hosts, auto_nets, syslog,
daemon)
finally:
try:
if daemon:
# it's not our child anymore; can't waitpid
fw.p.returncode = 0
fw.done()
finally:
if daemon:
daemon_cleanup()

231
sshuttle/firewall.py Normal file
View 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)

88
sshuttle/helpers.py Normal file
View File

@ -0,0 +1,88 @@
import sys
import socket
import errno
logprefix = ''
verbose = 0
def log(s):
try:
sys.stdout.flush()
sys.stderr.write(logprefix + s)
sys.stderr.flush()
except IOError:
# this could happen if stderr gets forcibly disconnected, eg. because
# our tty closes. That sucks, but it's no reason to abort the program.
pass
def debug1(s):
if verbose >= 1:
log(s)
def debug2(s):
if verbose >= 2:
log(s)
def debug3(s):
if verbose >= 3:
log(s)
class Fatal(Exception):
pass
def resolvconf_nameservers():
l = []
for line in open('/etc/resolv.conf'):
words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver':
l.append(family_ip_tuple(words[1]))
return l
def resolvconf_random_nameserver():
l = resolvconf_nameservers()
if l:
if len(l) > 1:
# don't import this unless we really need it
import random
random.shuffle(l)
return l[0]
else:
return (socket.AF_INET, '127.0.0.1')
def islocal(ip, family):
sock = socket.socket(family)
try:
try:
sock.bind((ip, 0))
except socket.error as e:
if e.args[0] == errno.EADDRNOTAVAIL:
return False # not a local IP
else:
raise
finally:
sock.close()
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):
if family == socket.AF_INET6:
return "AF_INET6"
elif family == socket.AF_INET:
return "AF_INET"
else:
return str(family)

View File

@ -1,18 +1,29 @@
import subprocess, time, socket, re, select import time
if not globals().get('skip_imports'): import socket
import helpers import re
from helpers import * import select
import errno
import os
import sys
POLL_TIME = 60*15 import subprocess as ssubprocess
import sshuttle.helpers as helpers
from sshuttle.helpers import log, debug1, debug2, debug3
POLL_TIME = 60 * 15
NETSTAT_POLL_TIME = 30 NETSTAT_POLL_TIME = 30
CACHEFILE=os.path.expanduser('~/.sshuttle.hosts') CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
_nmb_ok = True _nmb_ok = True
_smb_ok = True _smb_ok = True
hostnames = {} hostnames = {}
queue = {} queue = {}
null = open('/dev/null', 'rb+') try:
null = open('/dev/null', 'wb')
except IOError as e:
log('warning: %s\n' % e)
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096)
def _is_ip(s): def _is_ip(s):
@ -23,7 +34,7 @@ def write_host_cache():
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid()) tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
try: try:
f = open(tmpname, 'wb') f = open(tmpname, 'wb')
for name,ip in sorted(hostnames.items()): for name, ip in sorted(hostnames.items()):
f.write('%s,%s\n' % (name, ip)) f.write('%s,%s\n' % (name, ip))
f.close() f.close()
os.rename(tmpname, CACHEFILE) os.rename(tmpname, CACHEFILE)
@ -37,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:
@ -45,18 +56,18 @@ def read_host_cache():
for line in f: for line in f:
words = line.strip().split(',') words = line.strip().split(',')
if len(words) == 2: if len(words) == 2:
(name,ip) = words (name, ip) = words
name = re.sub(r'[^-\w]', '-', name).strip() name = re.sub(r'[^-\w]', '-', name).strip()
ip = re.sub(r'[^0-9.]', '', ip).strip() ip = re.sub(r'[^0-9.]', '', ip).strip()
if name and ip: if name and ip:
found_host(name, ip) found_host(name, ip)
def found_host(hostname, ip): def found_host(hostname, ip):
hostname = re.sub(r'\..*', '', hostname) hostname = re.sub(r'\..*', '', hostname)
hostname = re.sub(r'[^-\w]', '_', hostname) hostname = re.sub(r'[^-\w]', '_', hostname)
if (ip.startswith('127.') or ip.startswith('255.') if (ip.startswith('127.') or ip.startswith('255.')
or hostname == 'localhost'): or hostname == 'localhost'):
return return
oldip = hostnames.get(hostname) oldip = hostnames.get(hostname)
if oldip != ip: if oldip != ip:
@ -89,7 +100,7 @@ def _check_revdns(ip):
debug3('< %s\n' % r[0]) debug3('< %s\n' % r[0])
check_host(r[0]) check_host(r[0])
found_host(r[0], ip) found_host(r[0], ip)
except socket.herror, e: except socket.herror:
pass pass
@ -100,7 +111,7 @@ def _check_dns(hostname):
debug3('< %s\n' % ip) debug3('< %s\n' % ip)
check_host(ip) check_host(ip)
found_host(hostname, ip) found_host(hostname, ip)
except socket.gaierror, e: except socket.gaierror:
pass pass
@ -108,17 +119,17 @@ def _check_netstat():
debug2(' > netstat\n') debug2(' > netstat\n')
argv = ['netstat', '-n'] argv = ['netstat', '-n']
try: try:
p = subprocess.Popen(argv, stdout=subprocess.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
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content): for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
debug3('< %s\n' % ip) debug3('< %s\n' % ip)
check_host(ip) check_host(ip)
def _check_smb(hostname): def _check_smb(hostname):
return return
@ -128,10 +139,10 @@ def _check_smb(hostname):
argv = ['smbclient', '-U', '%', '-L', hostname] argv = ['smbclient', '-U', '%', '-L', hostname]
debug2(' > smb: %s\n' % hostname) debug2(' > smb: %s\n' % hostname)
try: try:
p = subprocess.Popen(argv, stdout=subprocess.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
@ -182,13 +193,13 @@ def _check_nmb(hostname, is_workgroup, is_master):
global _nmb_ok global _nmb_ok
if not _nmb_ok: if not _nmb_ok:
return return
argv = ['nmblookup'] + ['-M']*is_master + ['--', hostname] argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname]
debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname)) debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname))
try: try:
p = subprocess.Popen(argv, stdout=subprocess.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
@ -223,13 +234,13 @@ def check_workgroup(hostname):
def _enqueue(op, *args): def _enqueue(op, *args):
t = (op,args) t = (op, args)
if queue.get(t) == None: if queue.get(t) is None:
queue[t] = 0 queue[t] = 0
def _stdin_still_ok(timeout): def _stdin_still_ok(timeout):
r,w,x = select.select([sys.stdin.fileno()], [], [], timeout) r, w, x = select.select([sys.stdin.fileno()], [], [], timeout)
if r: if r:
b = os.read(sys.stdin.fileno(), 4096) b = os.read(sys.stdin.fileno(), 4096)
if not b: if not b:
@ -244,7 +255,7 @@ def hw_main(seed_hosts):
helpers.logprefix = 'hostwatch: ' helpers.logprefix = 'hostwatch: '
read_host_cache() read_host_cache()
_enqueue(_check_etc_hosts) _enqueue(_check_etc_hosts)
_enqueue(_check_netstat) _enqueue(_check_netstat)
check_host('localhost') check_host('localhost')
@ -256,8 +267,8 @@ 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
maxtime = POLL_TIME maxtime = POLL_TIME
@ -270,7 +281,7 @@ def hw_main(seed_hosts):
sys.stdout.flush() sys.stdout.flush()
except IOError: except IOError:
break break
# FIXME: use a smarter timeout based on oldest last_polled # FIXME: use a smarter timeout based on oldest last_polled
if not _stdin_still_ok(1): if not _stdin_still_ok(1):
break break

62
sshuttle/linux.py Normal file
View 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)

View 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
View 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
View 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
View 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
View 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")

215
sshuttle/options.py Normal file
View File

@ -0,0 +1,215 @@
"""Command-line options parser.
With the help of an options spec string, easily parse command-line options.
"""
import sys
import os
import textwrap
import getopt
import re
import struct
class OptDict:
def __init__(self):
self._opts = {}
def __setitem__(self, k, v):
if k.startswith('no-') or k.startswith('no_'):
k = k[3:]
v = not v
self._opts[k] = v
def __getitem__(self, k):
if k.startswith('no-') or k.startswith('no_'):
return not self._opts[k[3:]]
return self._opts[k]
def __getattr__(self, k):
return self[k]
def _default_onabort(msg):
sys.exit(97)
def _intify(v):
try:
vv = int(v or '')
if str(vv) == v:
return vv
except ValueError:
pass
return v
def _atoi(v):
try:
return int(v or 0)
except ValueError:
return 0
def _remove_negative_kv(k, v):
if k.startswith('no-') or k.startswith('no_'):
return k[3:], not v
return k, v
def _remove_negative_k(k):
return _remove_negative_kv(k, None)[0]
def _tty_width():
if not hasattr(sys.stderr, "fileno"):
return _atoi(os.environ.get('WIDTH')) or 70
s = struct.pack("HHHH", 0, 0, 0, 0)
try:
import fcntl
import termios
s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
except (IOError, ImportError):
return _atoi(os.environ.get('WIDTH')) or 70
(ysize, xsize, ypix, xpix) = struct.unpack('HHHH', s)
return xsize or 70
class Options:
"""Option parser.
When constructed, two strings are mandatory. The first one is the command
name showed before error messages. The second one is a string called an
optspec that specifies the synopsis and option flags and their description.
For more information about optspecs, consult the bup-options(1) man page.
Two optional arguments specify an alternative parsing function and an
alternative behaviour on abort (after having output the usage string).
By default, the parser function is getopt.gnu_getopt, and the abort
behaviour is to exit the program.
"""
def __init__(self, optspec, optfunc=getopt.gnu_getopt,
onabort=_default_onabort):
self.optspec = optspec
self._onabort = onabort
self.optfunc = optfunc
self._aliases = {}
self._shortopts = 'h?'
self._longopts = ['help']
self._hasparms = {}
self._defaults = {}
self._usagestr = self._gen_usage()
def _gen_usage(self):
out = []
lines = self.optspec.strip().split('\n')
lines.reverse()
first_syn = True
while lines:
l = lines.pop()
if l == '--':
break
out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
first_syn = False
out.append('\n')
last_was_option = False
while lines:
l = lines.pop()
if l.startswith(' '):
out.append('%s%s\n' % (last_was_option and '\n' or '',
l.lstrip()))
last_was_option = False
elif l:
(flags, extra) = l.split(' ', 1)
extra = extra.strip()
if flags.endswith('='):
flags = flags[:-1]
has_parm = 1
else:
has_parm = 0
g = re.search(r'\[([^\]]*)\]$', extra)
if g:
defval = g.group(1)
else:
defval = None
flagl = flags.split(',')
flagl_nice = []
for _f in flagl:
f, dvi = _remove_negative_kv(_f, _intify(defval))
self._aliases[f] = _remove_negative_k(flagl[0])
self._hasparms[f] = has_parm
self._defaults[f] = dvi
if len(f) == 1:
self._shortopts += f + (has_parm and ':' or '')
flagl_nice.append('-' + f)
else:
f_nice = re.sub(r'\W', '_', f)
self._aliases[f_nice] = _remove_negative_k(flagl[0])
self._longopts.append(f + (has_parm and '=' or ''))
self._longopts.append('no-' + f)
flagl_nice.append('--' + _f)
flags_nice = ', '.join(flagl_nice)
if has_parm:
flags_nice += ' ...'
prefix = ' %-20s ' % flags_nice
argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
initial_indent=prefix,
subsequent_indent=' ' * 28))
out.append(argtext + '\n')
last_was_option = True
else:
out.append('\n')
last_was_option = False
return ''.join(out).rstrip() + '\n'
def usage(self, msg=""):
"""Print usage string to stderr and abort."""
sys.stderr.write(self._usagestr)
e = self._onabort and self._onabort(msg) or None
if e:
raise e
def fatal(self, s):
"""Print an error message to stderr and abort with usage string."""
msg = 'error: %s\n' % s
sys.stderr.write(msg)
return self.usage(msg)
def parse(self, args):
"""Parse a list of arguments and return (options, flags, extra).
In the returned tuple, "options" is an OptDict with known options,
"flags" is a list of option flags that were used on the command-line,
and "extra" is a list of positional arguments.
"""
try:
(flags, extra) = self.optfunc(
args, self._shortopts, self._longopts)
except getopt.GetoptError as e:
self.fatal(e)
opt = OptDict()
for k, v in self._defaults.items():
k = self._aliases[k]
opt[k] = v
for (k, v) in flags:
k = k.lstrip('-')
if k in ('h', '?', 'help'):
self.usage()
if k.startswith('no-'):
k = self._aliases[k[3:]]
v = 0
else:
k = self._aliases[k]
if not self._hasparms[k]:
assert(v == '')
v = (opt._opts.get(k) or 0) + 1
else:
v = _intify(v)
opt[k] = v
for (f1, f2) in self._aliases.items():
opt[f1] = opt._opts.get(f2)
return (opt, flags, extra)

337
sshuttle/server.py Normal file
View File

@ -0,0 +1,337 @@
import re
import struct
import socket
import traceback
import time
import sys
import os
import sshuttle.ssnet as ssnet
import sshuttle.helpers as helpers
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):
if ipstr == 'default':
ipstr = '0.0.0.0/0'
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m:
g = m.groups()
ips = g[0]
width = int(g[4] or 32)
if g[1] is None:
ips += '.0.0.0'
width = min(width, 8)
elif g[2] is None:
ips += '.0.0'
width = min(width, 16)
elif g[3] is None:
ips += '.0'
width = min(width, 24)
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
def _ipstr(ip, width):
if width >= 32:
return ip
else:
return "%s/%d" % (ip, width)
def _maskbits(netmask):
if not netmask:
return 32
for i in range(32):
if netmask[0] & _shl(1, i):
return 32 - i
return 0
def _shl(n, bits):
return n * int(2 ** bits)
def _list_routes():
argv = ['netstat', '-rn']
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
routes = []
for line in p.stdout:
cols = re.split(r'\s+', line)
ipw = _ipmatch(cols[0])
if not ipw:
continue # some lines won't be parseable; never mind
maskw = _ipmatch(cols[2]) # linux only
mask = _maskbits(maskw) # returns 32 if maskw is null
width = min(ipw[1], mask)
ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width)
routes.append(
(socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width))
rv = p.wait()
if rv != 0:
log('WARNING: %r returned %d\n' % (argv, rv))
log('WARNING: That prevents --auto-nets from working.\n')
return routes
def list_routes():
for (family, ip, width) in _list_routes():
if not ip.startswith('0.') and not ip.startswith('127.'):
yield (family, ip, width)
def _exc_dump():
exc_info = sys.exc_info()
return ''.join(traceback.format_exception(*exc_info))
def start_hostwatch(seed_hosts):
s1, s2 = socket.socketpair()
pid = os.fork()
if not pid:
# child
rv = 99
try:
try:
s2.close()
os.dup2(s1.fileno(), 1)
os.dup2(s1.fileno(), 0)
s1.close()
rv = hostwatch.hw_main(seed_hosts) or 0
except Exception:
log('%s\n' % _exc_dump())
rv = 98
finally:
os._exit(rv)
s1.close()
return pid, s2
class Hostwatch:
def __init__(self):
self.pid = 0
self.sock = None
class DnsProxy(Handler):
def __init__(self, mux, chan, request):
Handler.__init__(self, [])
self.timeout = time.time() + 30
self.mux = mux
self.chan = chan
self.tries = 0
self.request = request
self.peers = {}
self.try_send()
def try_send(self):
if self.tries >= 3:
return
self.tries += 1
family, peer = resolvconf_random_nameserver()
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:
sock.send(self.request)
self.socks.append(sock)
except socket.error as e:
if e.args[0] in ssnet.NET_ERRS:
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS send to %r: %s\n' % (peer, e))
self.try_send()
return
else:
log('DNS send to %r: %s\n' % (peer, e))
return
def callback(self, sock):
peer = self.peers[sock]
try:
data = sock.recv(4096)
except socket.error as e:
self.socks.remove(sock)
del self.peers[sock]
if e.args[0] in ssnet.NET_ERRS:
# might have been spurious; try again.
# Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both.
debug2('DNS recv from %r: %s\n' % (peer, e))
self.try_send()
return
else:
log('DNS recv from %r: %s\n' % (peer, e))
return
debug2('DNS response: %d bytes\n' % len(data))
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
self.ok = False
class UdpProxy(Handler):
def __init__(self, mux, chan, family):
sock = socket.socket(family, socket.SOCK_DGRAM)
Handler.__init__(self, [sock])
self.timeout = time.time() + 30
self.mux = mux
self.chan = chan
self.sock = sock
if family == socket.AF_INET:
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
def send(self, dstip, data):
debug2('UDP: sending to %r port %d\n' % dstip)
try:
self.sock.sendto(data, dstip)
except socket.error as e:
log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
return
def callback(self, sock):
try:
data, peer = sock.recvfrom(4096)
except socket.error as e:
log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
return
debug2('UDP response: %d bytes\n' % len(data))
hdr = "%s,%r," % (peer[0], peer[1])
self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
def main(latency_control):
if helpers.verbose >= 1:
helpers.logprefix = ' s: '
else:
helpers.logprefix = 'server: '
debug1('latency control setting = %r\n' % latency_control)
routes = list(list_routes())
debug1('available routes:\n')
for r in routes:
debug1(' %d/%s/%d\n' % r)
# synchronization header
sys.stdout.write('\0\0SSHUTTLE0001')
sys.stdout.flush()
handlers = []
mux = Mux(socket.fromfd(sys.stdin.fileno(),
socket.AF_INET, socket.SOCK_STREAM),
socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM))
handlers.append(mux)
routepkt = ''
for r in routes:
routepkt += '%d,%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch()
hw.leftover = ''
def hostwatch_ready(sock):
assert(hw.pid)
content = hw.sock.recv(4096)
if content:
lines = (hw.leftover + content).split('\n')
if lines[-1]:
# no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop()
lines.append('')
else:
hw.leftover = ''
mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines))
else:
raise Fatal('hostwatch process died')
def got_host_req(data):
if not hw.pid:
(hw.pid, hw.sock) = start_hostwatch(data.strip().split())
handlers.append(Handler(socks=[hw.sock],
callback=hostwatch_ready))
mux.got_host_req = got_host_req
def new_channel(channel, data):
(family, dstip, dstport) = data.split(',', 2)
family = int(family)
dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel
dnshandlers = {}
def dns_req(channel, data):
debug2('Incoming DNS request channel=%d.\n' % channel)
h = DnsProxy(mux, channel, data)
handlers.append(h)
dnshandlers[channel] = h
mux.got_dns_req = dns_req
udphandlers = {}
def udp_req(channel, cmd, data):
debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd))
if cmd == ssnet.CMD_UDP_DATA:
(dstip, dstport, data) = data.split(",", 2)
dstport = int(dstport)
debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport))
h = udphandlers[channel]
h.send((dstip, dstport), data)
elif cmd == ssnet.CMD_UDP_CLOSE:
debug2('is incoming UDP close\n')
h = udphandlers[channel]
h.ok = False
del mux.channels[channel]
def udp_open(channel, data):
debug2('Incoming UDP open.\n')
family = int(data)
mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data)
if channel in udphandlers:
raise Fatal('UDP connection channel %d already open' % channel)
else:
h = UdpProxy(mux, channel, family)
handlers.append(h)
udphandlers[channel] = h
mux.got_udp_open = udp_open
while mux.ok:
if hw.pid:
assert(hw.pid > 0)
(rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
if rpid:
raise Fatal(
'hostwatch exited unexpectedly: code 0x%04x\n' % rv)
ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
if dnshandlers:
now = time.time()
for channel, h in list(dnshandlers.items()):
if h.timeout < now or not h.ok:
debug3('expiring dnsreqs channel=%d\n' % channel)
del dnshandlers[channel]
h.ok = False
if udphandlers:
for channel, h in list(udphandlers.items()):
if not h.ok:
debug3('expiring UDP channel=%d\n' % channel)
del udphandlers[channel]
h.ok = False

129
sshuttle/ssh.py Normal file
View File

@ -0,0 +1,129 @@
import sys
import os
import re
import socket
import zlib
import imp
import subprocess as ssubprocess
import sshuttle.helpers as helpers
from sshuttle.helpers import debug2
def readfile(name):
tokens = name.split(".")
f = None
token = tokens[0]
token_name = [token]
token_str = ".".join(token_name)
try:
f, pathname, description = imp.find_module(token_str)
for token in tokens[1:]:
module = imp.load_module(token_str, f, pathname, description)
if f is not None:
f.close()
token_name.append(token)
token_str = ".".join(token_name)
f, pathname, description = imp.find_module(
token, module.__path__)
if f is not None:
contents = f.read()
else:
contents = ""
finally:
if f is not None:
f.close()
return contents.encode("UTF8")
def empackage(z, name, data=None):
if not data:
data = readfile(name)
content = z.compress(data)
content += z.flush(zlib.Z_SYNC_FLUSH)
return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content)
def connect(ssh_cmd, rhostport, python, stderr, options):
portl = []
if (rhostport or '').count(':') > 1:
if rhostport.count(']') or rhostport.count('['):
result = rhostport.split(']')
rhost = result[0].strip('[')
if len(result) > 1:
result[1] = result[1].strip(':')
if result[1] is not '':
portl = ['-p', str(int(result[1]))]
# can't disambiguate IPv6 colons and a port number. pass the hostname
# through.
else:
rhost = rhostport
else: # IPv4
l = (rhostport or '').split(':', 1)
rhost = l[0]
if len(l) > 1:
portl = ['-p', str(int(l[1]))]
if rhost == '-':
rhost = None
z = zlib.compressobj(1)
content = readfile('sshuttle.assembler')
optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
optdata = optdata.encode("UTF8")
content2 = (empackage(z, 'sshuttle') +
empackage(z, 'sshuttle.cmdline_options', optdata) +
empackage(z, 'sshuttle.helpers') +
empackage(z, 'sshuttle.ssnet') +
empackage(z, 'sshuttle.hostwatch') +
empackage(z, 'sshuttle.server') +
b"\n")
pyscript = r"""
import sys;
verbosity=%d;
exec compile(sys.stdin.read(%d), "assembler.py", "exec")
""" % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
if not rhost:
# ignore the --python argument when running locally; we already know
# which python version works.
argv = [sys.argv[1], '-c', pyscript]
else:
if ssh_cmd:
sshl = ssh_cmd.split(' ')
else:
sshl = ['ssh']
if python:
pycmd = "'%s' -c '%s'" % (python, pyscript)
else:
pycmd = ("P=python2; $P -V 2>/dev/null || P=python; "
"exec \"$P\" -c '%s'") % pyscript
argv = (sshl +
portl +
[rhost, '--', pycmd])
(s1, s2) = socket.socketpair()
def setup():
# runs in the child process
s2.close()
s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close()
debug2('executing: %r\n' % argv)
p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True, stderr=stderr)
os.close(s1a)
os.close(s1b)
s2.sendall(content)
s2.sendall(content2)
return p, s2

279
sshuttle/sshuttle.md Normal file
View File

@ -0,0 +1,279 @@
% sshuttle(8) Sshuttle 0.46
% Avery Pennarun <apenwarr@gmail.com>
% 2011-01-25
# NAME
sshuttle - a transparent proxy-based VPN using ssh
# SYNOPSIS
sshuttle [options...] [-r [username@]sshserver[:port]] \<subnets...\>
# DESCRIPTION
sshuttle allows you to create a VPN connection from your
machine to any remote server that you can connect to via
ssh, as long as that server has python 2.3 or higher.
To work, you must have root access on the local machine,
but you can have a normal account on the server.
It's valid to run sshuttle more than once simultaneously on
a single client machine, connecting to a different server
every time, so you can be on more than one VPN at once.
If run on a router, sshuttle can forward traffic for your
entire subnet to the VPN.
# OPTIONS
\<subnets...\>
: a list of subnets to route over the VPN, in the form
`a.b.c.d[/width]`. Valid examples are 1.2.3.4 (a
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
netmask), and 0/0 ('just route everything through the
VPN').
-l, --listen=*[ip:]port*
: use this ip address and port number as the transparent
proxy port. By default sshuttle finds an available
port automatically and listens on IP 127.0.0.1
(localhost), so you don't need to override it, and
connections are only proxied from the local machine,
not from outside machines. If you want to accept
connections from other machines on your network (ie. to
run sshuttle on a router) try enabling IP Forwarding in
your kernel, then using `--listen 0.0.0.0:0`.
-H, --auto-hosts
: scan for remote hostnames and update the local /etc/hosts
file with matching entries for as long as the VPN is
open. This is nicer than changing your system's DNS
(/etc/resolv.conf) settings, for several reasons. First,
hostnames are added without domain names attached, so
you can `ssh thatserver` without worrying if your local
domain matches the remote one. Second, if you sshuttle
into more than one VPN at a time, it's impossible to
use more than one DNS server at once anyway, but
sshuttle correctly merges /etc/hosts entries between
all running copies. Third, if you're only routing a
few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else.
-N, --auto-nets
: in addition to the subnets provided on the command
line, ask the server which subnets it thinks we should
route, and route those automatically. The suggestions
are taken automatically from the server's routing
table.
--dns
: capture local DNS requests and forward to the remote DNS
server.
--python
: specify the name/path of the remote python interpreter.
The default is just `python`, which means to use the
default python interpreter on the remote system's PATH.
-r, --remote=*[username@]sshserver[:port]*
: the remote hostname and optional username and ssh
port number to use for connecting to the remote server.
For example, example.com, testuser@example.com,
testuser@example.com:2222, or example.com:2244.
-x, --exclude=*subnet*
: explicitly exclude this subnet from forwarding. The
format of this option is the same as the `<subnets>`
option. To exclude more than one subnet, specify the
`-x` option more than once. You can say something like
`0/0 -x 1.2.3.0/24` to forward everything except the
local subnet over the VPN, for example.
-X, --exclude-from=*file*
: exclude the subnets specified in a file, one subnet per
line. Useful when you have lots of subnets to exclude.
-v, --verbose
: print more information about the session. This option
can be used more than once for increased verbosity. By
default, sshuttle prints only error messages.
-e, --ssh-cmd
: the command to use to connect to the remote server. The
default is just `ssh`. Use this if your ssh client is
in a non-standard location or you want to provide extra
options to the ssh command, for example, `-e 'ssh -v'`.
--seed-hosts
: a comma-separated list of hostnames to use to
initialize the `--auto-hosts` scan algorithm.
`--auto-hosts` does things like poll local SMB servers
for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start
from.
--no-latency-control
: sacrifice latency to improve bandwidth benchmarks. ssh
uses really big socket buffers, which can overload the
connection if you start doing large file transfers,
thus making all your other sessions inside the same
tunnel go slowly. Normally, sshuttle tries to avoid
this problem using a "fullness check" that allows only
a certain amount of outstanding data to be buffered at
a time. But on high-bandwidth links, this can leave a
lot of your bandwidth underutilized. It also makes
sshuttle seem slow in bandwidth benchmarks (benchmarks
rarely test ping latency, which is what sshuttle is
trying to control). This option disables the latency
control feature, maximizing bandwidth usage. Use at
your own risk.
-D, --daemon
: automatically fork into the background after connecting
to the remote server. Implies `--syslog`.
--syslog
: after connecting, send all log messages to the
`syslog`(3) service instead of stderr. This is
implicit if you use `--daemon`.
--pidfile=*pidfilename*
: when using `--daemon`, save sshuttle's pid to
*pidfilename*. The default is `sshuttle.pid` in the
current directory.
--firewall
: (internal use only) run the firewall manager. This is
the only part of sshuttle that must run as root. If
you start sshuttle as a non-root user, it will
automatically run `sudo` or `su` to start the firewall
manager, but the core of sshuttle still runs as a
normal user.
--hostwatch
: (internal use only) run the hostwatch daemon. This
process runs on the server side and collects hostnames for
the `--auto-hosts` option. Using this option by itself
makes it a lot easier to debug and test the `--auto-hosts`
feature.
# EXAMPLES
Test locally by proxying all local connections, without using ssh:
$ sshuttle -v 0/0
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
[local sudo] Password:
firewall manager ready.
c : connecting to server...
s: available routes:
s: 192.168.42.0/24
c : connected.
firewall manager: starting transproxy.
c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139.
c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443.
...etc...
^C
firewall manager: undoing changes.
KeyboardInterrupt
c : Keyboard interrupt: exiting.
c : SW#8:192.168.42.121:47523: deleting
c : SW#6:192.168.42.106:50035: deleting
Test connection to a remote server, with automatic hostname
and subnet guessing:
$ sshuttle -vNHr example.org
Starting sshuttle proxy.
Listening on ('0.0.0.0', 12300).
firewall manager ready.
c : connecting to server...
s: available routes:
s: 77.141.99.0/24
c : connected.
c : seed_hosts: []
firewall manager: starting transproxy.
hostwatch: Found: testbox1: 1.2.3.4
hostwatch: Found: mytest2: 5.6.7.8
hostwatch: Found: domaincontroller: 99.1.2.3
c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22.
^C
firewall manager: undoing changes.
c : Keyboard interrupt: exiting.
c : SW#6:192.168.42.121:60554: deleting
# DISCUSSION
When it starts, sshuttle creates an ssh session to the
server specified by the `-r` option. If `-r` is omitted,
it will start both its client and server locally, which is
sometimes useful for testing.
After connecting to the remote server, sshuttle uploads its
(python) source code to the remote end and executes it
there. Thus, you don't need to install sshuttle on the
remote server, and there are never sshuttle version
conflicts between client and server.
Unlike most VPNs, sshuttle forwards sessions, not packets.
That is, it uses kernel transparent proxying (`iptables
REDIRECT` rules on Linux, or `ipfw fwd` rules on BSD) to
capture outgoing TCP sessions, then creates entirely
separate TCP sessions out to the original destination at
the other end of the tunnel.
Packet-level forwarding (eg. using the tun/tap devices on
Linux) seems elegant at first, but it results in
several problems, notably the 'tcp over tcp' problem. The
tcp protocol depends fundamentally on packets being dropped
in order to implement its congestion control agorithm; if
you pass tcp packets through a tcp-based tunnel (such as
ssh), the inner tcp packets will never be dropped, and so
the inner tcp stream's congestion control will be
completely broken, and performance will be terrible. Thus,
packet-based VPNs (such as IPsec and openvpn) cannot use
tcp-based encrypted streams like ssh or ssl, and have to
implement their own encryption from scratch, which is very
complex and error prone.
sshuttle's simplicity comes from the fact that it can
safely use the existing ssh encrypted tunnel without
incurring a performance penalty. It does this by letting
the client-side kernel manage the incoming tcp stream, and
the server-side kernel manage the outgoing tcp stream;
there is no need for congestion control to be shared
between the two separate streams, so a tcp-based tunnel is
fine.
# BUGS
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.
On MacOS, sshuttle will set the kernel boot flag
net.inet.ip.scopedroute to 0, which interferes with OS X
Internet Sharing and some VPN clients. To reset this flag,
you can remove any reference to net.inet.ip.scopedroute from
/Library/Preferences/SystemConfiguration/com.apple.Boot.plist
and reboot.
# SEE ALSO
`ssh`(1), `python`(1)

View File

@ -1,6 +1,17 @@
import struct, socket, errno, select import struct
if not globals().get('skip_imports'): import socket
from helpers import * import errno
import select
import os
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal
MAX_CHANNEL = 65535
# these don't exist in the socket module in python 2.3!
SHUT_RD = 0
SHUT_WR = 1
SHUT_RDWR = 2
HDR_LEN = 8 HDR_LEN = 8
@ -8,36 +19,67 @@ HDR_LEN = 8
CMD_EXIT = 0x4200 CMD_EXIT = 0x4200
CMD_PING = 0x4201 CMD_PING = 0x4201
CMD_PONG = 0x4202 CMD_PONG = 0x4202
CMD_CONNECT = 0x4203 CMD_TCP_CONNECT = 0x4203
CMD_CLOSE = 0x4204 CMD_TCP_STOP_SENDING = 0x4204
CMD_EOF = 0x4205 CMD_TCP_EOF = 0x4205
CMD_DATA = 0x4206 CMD_TCP_DATA = 0x4206
CMD_ROUTES = 0x4207 CMD_ROUTES = 0x4207
CMD_HOST_REQ = 0x4208 CMD_HOST_REQ = 0x4208
CMD_HOST_LIST = 0x4209 CMD_HOST_LIST = 0x4209
CMD_DNS_REQ = 0x420a
CMD_DNS_RESPONSE = 0x420b
CMD_UDP_OPEN = 0x420c
CMD_UDP_DATA = 0x420d
CMD_UDP_CLOSE = 0x420e
cmd_to_name = { cmd_to_name = {
CMD_EXIT: 'EXIT', CMD_EXIT: 'EXIT',
CMD_PING: 'PING', CMD_PING: 'PING',
CMD_PONG: 'PONG', CMD_PONG: 'PONG',
CMD_CONNECT: 'CONNECT', CMD_TCP_CONNECT: 'TCP_CONNECT',
CMD_CLOSE: 'CLOSE', CMD_TCP_STOP_SENDING: 'TCP_STOP_SENDING',
CMD_EOF: 'EOF', CMD_TCP_EOF: 'TCP_EOF',
CMD_DATA: 'DATA', CMD_TCP_DATA: 'TCP_DATA',
CMD_ROUTES: 'ROUTES', CMD_ROUTES: 'ROUTES',
CMD_HOST_REQ: 'HOST_REQ', CMD_HOST_REQ: 'HOST_REQ',
CMD_HOST_LIST: 'HOST_LIST', CMD_HOST_LIST: 'HOST_LIST',
CMD_DNS_REQ: 'DNS_REQ',
CMD_DNS_RESPONSE: 'DNS_RESPONSE',
CMD_UDP_OPEN: 'UDP_OPEN',
CMD_UDP_DATA: 'UDP_DATA',
CMD_UDP_CLOSE: 'UDP_CLOSE',
} }
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
errno.EHOSTUNREACH, errno.ENETUNREACH,
errno.EHOSTDOWN, errno.ENETDOWN]
def _add(l, elem):
if elem not in l:
l.append(elem)
def _fds(l):
out = []
for i in l:
try:
out.append(i.fileno())
except AttributeError:
out.append(i)
out.sort()
return out
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:
debug3('%s: err was: %s\n' % (func.__name__, e))
return None return None
@ -46,14 +88,21 @@ 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'
_swcount = 0
class SockWrapper: class SockWrapper:
def __init__(self, rsock, wsock, connect_to=None, peername=None): def __init__(self, rsock, wsock, connect_to=None, peername=None):
global _swcount
_swcount += 1
debug3('creating new SockWrapper (%d now exist)\n' % _swcount)
self.exc = None self.exc = None
self.rsock = rsock self.rsock = rsock
self.wsock = wsock self.wsock = wsock
@ -64,34 +113,66 @@ class SockWrapper:
self.try_connect() self.try_connect()
def __del__(self): def __del__(self):
debug1('%r: deleting\n' % self) global _swcount
_swcount -= 1
debug1('%r: deleting (%d remain)\n' % (self, _swcount))
if self.exc: if self.exc:
debug1('%r: error was: %r\n' % (self, self.exc)) debug1('%r: error was: %s\n' % (self, self.exc))
def __repr__(self): def __repr__(self):
return 'SW:%s' % (self.peername,) if self.rsock == self.wsock:
fds = '#%d' % self.rsock.fileno()
else:
fds = '#%d,%d' % (self.rsock.fileno(), self.wsock.fileno())
return 'SW%s:%s' % (fds, self.peername)
def seterr(self, e): def seterr(self, e):
if not self.exc: if not self.exc:
self.exc = e self.exc = e
self.nowrite()
self.noread()
def try_connect(self): def try_connect(self):
if self.connect_to and self.shut_write:
self.noread()
self.connect_to = None
if not self.connect_to: if not self.connect_to:
return # already connected return # already connected
self.rsock.setblocking(False) self.rsock.setblocking(False)
debug3('%r: trying connect to %r\n' % (self, self.connect_to))
try: try:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux) # connected successfully (Linux)
self.connect_to = None self.connect_to = None
except socket.error, e: except socket.error as e:
debug3('%r: connect result: %s\n' % (self, e))
if e.args[0] == errno.EINVAL:
# this is what happens when you call connect() on a socket
# that is now connected but returned EINPROGRESS last time,
# on BSD, on python pre-2.5.1. We need to use getsockopt()
# to get the "real" error. Later pythons do this
# automatically, so this code won't run.
realerr = self.rsock.getsockopt(socket.SOL_SOCKET,
socket.SO_ERROR)
e = socket.error(realerr, os.strerror(realerr))
debug3('%r: fixed connect result: %s\n' % (self, e))
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet pass # not connected yet
elif e.args[0] == 0:
# connected successfully (weird Linux bug?)
# Sometimes Linux seems to return EINVAL when it isn't
# invalid. This *may* be caused by a race condition
# between connect() and getsockopt(SO_ERROR) (ie. it
# finishes connecting in between the two, so there is no
# longer an error). However, I'm not sure of that.
#
# I did get at least one report that the problem went away
# when we added this, however.
self.connect_to = None
elif e.args[0] == errno.EISCONN: elif e.args[0] == errno.EISCONN:
# connected successfully (BSD) # connected successfully (BSD)
self.connect_to = None self.connect_to = None
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT, elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]:
errno.EHOSTUNREACH, errno.ENETUNREACH,
errno.EACCES, errno.EPERM]:
# a "normal" kind of error # a "normal" kind of error
self.connect_to = None self.connect_to = None
self.seterr(e) self.seterr(e)
@ -102,16 +183,16 @@ class SockWrapper:
if not self.shut_read: if not self.shut_read:
debug2('%r: done reading\n' % self) debug2('%r: done reading\n' % self)
self.shut_read = True self.shut_read = True
#self.rsock.shutdown(socket.SHUT_RD) # doesn't do anything anyway # self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway
def nowrite(self): def nowrite(self):
if not self.shut_write: if not self.shut_write:
debug2('%r: done writing\n' % self) debug2('%r: done writing\n' % self)
self.shut_write = True self.shut_write = True
try: try:
self.wsock.shutdown(socket.SHUT_WR) self.wsock.shutdown(SHUT_WR)
except socket.error, e: except socket.error as e:
self.seterr(e) self.seterr('nowrite: %s' % e)
def too_full(self): def too_full(self):
return False # fullness is determined by the socket's select() state return False # fullness is determined by the socket's select() state
@ -122,13 +203,16 @@ 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:
# unexpected error... stream is dead if e.errno == errno.EPIPE:
self.seterr(e) debug1('%r: uwrite: got EPIPE\n' % self)
self.nowrite() self.nowrite()
self.noread() return 0
return 0 else:
# unexpected error... stream is dead
self.seterr('uwrite: %s' % e)
return 0
def write(self, buf): def write(self, buf):
assert(buf) assert(buf)
return self.uwrite(buf) return self.uwrite(buf)
@ -141,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(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:
@ -151,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):
@ -159,33 +243,36 @@ class SockWrapper:
wrote = outwrap.write(self.buf[0]) wrote = outwrap.write(self.buf[0])
self.buf[0] = self.buf[0][wrote:] self.buf[0] = self.buf[0][wrote:]
while self.buf and not self.buf[0]: while self.buf and not self.buf[0]:
self.buf[0:1] = [] self.buf.pop(0)
if not self.buf and self.shut_read: if not self.buf and self.shut_read:
outwrap.nowrite() outwrap.nowrite()
class Handler: class Handler:
def __init__(self, socks = None, callback = None):
def __init__(self, socks=None, callback=None):
self.ok = True self.ok = True
self.socks = set(socks or []) self.socks = socks or []
if callback: if callback:
self.callback = callback self.callback = callback
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
r |= self.socks for i in self.socks:
_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:
v = s.recv(4096) v = s.recv(4096)
if not v: if not v:
log('--closed-- %r\n' % self) log('--closed-- %r\n' % self)
self.socks = set() self.socks = []
self.ok = False self.ok = False
class Proxy(Handler): class Proxy(Handler):
def __init__(self, wrap1, wrap2): def __init__(self, wrap1, wrap2):
Handler.__init__(self, [wrap1.rsock, wrap1.wsock, Handler.__init__(self, [wrap1.rsock, wrap1.wsock,
wrap2.rsock, wrap2.wsock]) wrap2.rsock, wrap2.wsock])
@ -193,86 +280,104 @@ class Proxy(Handler):
self.wrap2 = wrap2 self.wrap2 = wrap2
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
if self.wrap1.shut_write:
self.wrap2.noread()
if self.wrap2.shut_write:
self.wrap1.noread()
if self.wrap1.connect_to: if self.wrap1.connect_to:
w.add(self.wrap1.rsock) _add(w, self.wrap1.rsock)
elif self.wrap1.buf: elif self.wrap1.buf:
if not self.wrap2.too_full(): if not self.wrap2.too_full():
w.add(self.wrap2.wsock) _add(w, self.wrap2.wsock)
elif not self.wrap1.shut_read: elif not self.wrap1.shut_read:
r.add(self.wrap1.rsock) _add(r, self.wrap1.rsock)
if self.wrap2.connect_to: if self.wrap2.connect_to:
w.add(self.wrap2.rsock) _add(w, self.wrap2.rsock)
elif self.wrap2.buf: elif self.wrap2.buf:
if not self.wrap1.too_full(): if not self.wrap1.too_full():
w.add(self.wrap1.wsock) _add(w, self.wrap1.wsock)
elif not self.wrap2.shut_read: elif not self.wrap2.shut_read:
r.add(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()
self.wrap2.fill() self.wrap2.fill()
self.wrap1.copy_to(self.wrap2) self.wrap1.copy_to(self.wrap2)
self.wrap2.copy_to(self.wrap1) self.wrap2.copy_to(self.wrap1)
if self.wrap1.buf and self.wrap2.shut_write:
self.wrap1.buf = []
self.wrap1.noread()
if self.wrap2.buf and self.wrap1.shut_write:
self.wrap2.buf = []
self.wrap2.noread()
if (self.wrap1.shut_read and self.wrap2.shut_read and if (self.wrap1.shut_read and self.wrap2.shut_read and
not self.wrap1.buf and not self.wrap2.buf): not self.wrap1.buf and not self.wrap2.buf):
self.ok = False self.ok = False
self.wrap1.nowrite()
self.wrap2.nowrite()
class Mux(Handler): class Mux(Handler):
def __init__(self, rsock, wsock): def __init__(self, rsock, wsock):
Handler.__init__(self, [rsock, wsock]) Handler.__init__(self, [rsock, wsock])
self.rsock = rsock self.rsock = rsock
self.wsock = wsock self.wsock = wsock
self.new_channel = self.got_routes = None self.new_channel = self.got_dns_req = self.got_routes = None
self.got_udp_open = self.got_udp_data = self.got_udp_close = None
self.got_host_req = self.got_host_list = None self.got_host_req = self.got_host_list = None
self.channels = {} self.channels = {}
self.chani = 0 self.chani = 0
self.want = 0 self.want = 0
self.inbuf = '' 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 > 65535: if self.chani > MAX_CHANNEL:
self.chani = 1 self.chani = 1
if not self.channels.get(self.chani): if not self.channels.get(self.chani):
return self.chani return self.chani
def amount_queued(self): def amount_queued(self):
return sum(len(b) for b in self.outbuf) total = 0
for b in self.outbuf:
total += len(b)
return total
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)),
len(data), self.fullness)) len(data), self.fullness))
self.fullness += len(data) self.fullness += len(data)
def got_packet(self, channel, cmd, data): def got_packet(self, channel, cmd, data):
debug2('< channel=%d cmd=%s len=%d\n' debug2('< channel=%d cmd=%s len=%d\n'
% (channel, cmd_to_name.get(cmd,hex(cmd)), len(data))) % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
if cmd == CMD_PING: if cmd == CMD_PING:
self.send(0, CMD_PONG, data) self.send(0, CMD_PONG, data)
elif cmd == CMD_PONG: elif cmd == CMD_PONG:
@ -281,10 +386,18 @@ class Mux(Handler):
self.fullness = 0 self.fullness = 0
elif cmd == CMD_EXIT: elif cmd == CMD_EXIT:
self.ok = False self.ok = False
elif cmd == CMD_CONNECT: elif cmd == CMD_TCP_CONNECT:
assert(not self.channels.get(channel)) assert(not self.channels.get(channel))
if self.new_channel: if self.new_channel:
self.new_channel(channel, data) self.new_channel(channel, data)
elif cmd == CMD_DNS_REQ:
assert(not self.channels.get(channel))
if self.got_dns_req:
self.got_dns_req(channel, data)
elif cmd == CMD_UDP_OPEN:
assert(not self.channels.get(channel))
if self.got_udp_open:
self.got_udp_open(channel, data)
elif cmd == CMD_ROUTES: elif cmd == CMD_ROUTES:
if self.got_routes: if self.got_routes:
self.got_routes(data) self.got_routes(data)
@ -301,14 +414,18 @@ class Mux(Handler):
else: else:
raise Exception('got CMD_HOST_LIST without got_host_list?') raise Exception('got CMD_HOST_LIST without got_host_list?')
else: else:
callback = self.channels[channel] callback = self.channels.get(channel)
callback(cmd, data) if not callback:
log('warning: closed channel %d got cmd=%s len=%d\n'
% (channel, cmd_to_name.get(cmd, hex(cmd)), len(data)))
else:
callback(cmd, data)
def flush(self): def flush(self):
self.wsock.setblocking(False) self.wsock.setblocking(False)
if self.outbuf and self.outbuf[0]: if self.outbuf and self.outbuf[0]:
wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0]) wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0])
debug2('mux wrote: %d/%d\n' % (wrote, len(self.outbuf[0]))) debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0])))
if wrote: if wrote:
self.outbuf[0] = self.outbuf[0][wrote:] self.outbuf[0] = self.outbuf[0][wrote:]
while self.outbuf and not self.outbuf[0]: while self.outbuf and not self.outbuf[0]:
@ -318,24 +435,24 @@ 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
def handle(self): def handle(self):
self.fill() self.fill()
#log('inbuf is: (%d,%d) %r\n' # log('inbuf is: (%d,%d) %r\n'
# % (self.want, len(self.inbuf), self.inbuf)) # % (self.want, len(self.inbuf), self.inbuf))
while 1: while 1:
if len(self.inbuf) >= (self.want or HDR_LEN): if len(self.inbuf) >= (self.want or HDR_LEN):
(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]
@ -346,12 +463,12 @@ class Mux(Handler):
break break
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
r.add(self.rsock) _add(r, self.rsock)
if self.outbuf: if self.outbuf:
w.add(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()
if self.outbuf and self.wsock in w: if self.outbuf and self.wsock in w:
@ -359,6 +476,7 @@ class Mux(Handler):
class MuxWrapper(SockWrapper): class MuxWrapper(SockWrapper):
def __init__(self, mux, channel): def __init__(self, mux, channel):
SockWrapper.__init__(self, mux.rsock, mux.wsock) SockWrapper.__init__(self, mux.rsock, mux.wsock)
self.mux = mux self.mux = mux
@ -372,16 +490,28 @@ class MuxWrapper(SockWrapper):
SockWrapper.__del__(self) SockWrapper.__del__(self)
def __repr__(self): def __repr__(self):
return 'SW%r:Mux#%d' % (self.peername,self.channel) return 'SW%r:Mux#%d' % (self.peername, self.channel)
def noread(self): def noread(self):
if not self.shut_read: 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, b'')
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_EOF, '') self.mux.send(self.channel, CMD_TCP_EOF, b'')
self.maybe_close()
def maybe_close(self):
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
# will then be able to reap our object.
self.mux.channels[self.channel] = None
def too_full(self): def too_full(self):
return self.mux.too_full return self.mux.too_full
@ -391,32 +521,59 @@ class MuxWrapper(SockWrapper):
return 0 # too much already enqueued return 0 # too much already enqueued
if len(buf) > 2048: if len(buf) > 2048:
buf = buf[:2048] buf = buf[:2048]
self.mux.send(self.channel, CMD_DATA, buf) self.mux.send(self.channel, CMD_TCP_DATA, buf)
return len(buf) return len(buf)
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
def got_packet(self, cmd, data): def got_packet(self, cmd, data):
if cmd == CMD_CLOSE: if cmd == CMD_TCP_EOF:
self.noread() self.noread()
elif cmd == CMD_TCP_STOP_SENDING:
self.nowrite() self.nowrite()
elif cmd == CMD_EOF: elif cmd == CMD_TCP_DATA:
self.noread()
elif cmd == CMD_DATA:
self.buf.append(data) self.buf.append(data)
else: else:
raise Exception('unknown command %d (%d bytes)' raise Exception('unknown command %d (%d bytes)'
% (cmd, len(data))) % (cmd, len(data)))
def connect_dst(ip, port): def connect_dst(family, ip, port):
debug2('Connecting to %s:%d\n' % (ip, port)) debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket() outsock = socket.socket(family)
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
return SockWrapper(outsock, outsock, return SockWrapper(outsock, outsock,
connect_to = (ip,port), connect_to=(ip, port),
peername = '%s:%d' % (ip,port)) peername = '%s:%d' % (ip, port))
def runonce(handlers, mux):
r = []
w = []
x = []
to_remove = [s for s in handlers if not s.ok]
for h in to_remove:
handlers.remove(h)
for s in handlers:
s.pre_select(r, w, x)
debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n'
% (len(handlers), _fds(r), _fds(w), _fds(x),
mux.fullness, mux.too_full))
(r, w, x) = select.select(r, w, x)
debug2(' Ready: %d r=%r w=%r x=%r\n'
% (len(handlers), _fds(r), _fds(w), _fds(x)))
ready = r + w + x
did = {}
for h in handlers:
for s in h.socks:
if s in ready:
h.callback(s)
did[s] = 1
for s in ready:
if s not in did:
raise Fatal('socket %r was not used by any handler' % s)

19
sshuttle/ssyslog.py Normal file
View File

@ -0,0 +1,19 @@
import sys
import os
import subprocess as ssubprocess
_p = None
def start_syslog():
global _p
_p = ssubprocess.Popen(['logger',
'-p', 'daemon.notice',
'-t', 'sshuttle'], stdin=ssubprocess.PIPE)
def stderr_to_syslog():
sys.stdout.flush()
sys.stderr.flush()
os.dup2(_p.stdin.fileno(), 2)

89
sshuttle/stresstest.py Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/env python
import socket
import select
import struct
import time
listener = socket.socket()
listener.bind(('127.0.0.1', 0))
listener.listen(500)
servers = []
clients = []
remain = {}
NUMCLIENTS = 50
count = 0
while 1:
if len(clients) < NUMCLIENTS:
c = socket.socket()
c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
c.bind(('0.0.0.0', 0))
c.connect(listener.getsockname())
count += 1
if count >= 16384:
count = 1
print('cli CREATING %d' % count)
b = struct.pack('I', count) + 'x' * count
remain[c] = count
print('cli >> %r' % len(b))
c.send(b)
c.shutdown(socket.SHUT_WR)
clients.append(c)
r = [listener]
time.sleep(0.1)
else:
r = [listener] + servers + clients
print('select(%d)' % len(r))
r, w, x = select.select(r, [], [], 5)
assert(r)
for i in r:
if i == listener:
s, addr = listener.accept()
servers.append(s)
elif i in servers:
b = i.recv(4096)
print('srv << %r' % len(b))
if i not in remain:
assert(len(b) >= 4)
want = struct.unpack('I', b[:4])[0]
b = b[4:]
# i.send('y'*want)
else:
want = remain[i]
if want < len(b):
print('weird wanted %d bytes, got %d: %r' % (want, len(b), b))
assert(want >= len(b))
want -= len(b)
remain[i] = want
if not b: # EOF
if want:
print('weird: eof but wanted %d more' % want)
assert(want == 0)
i.close()
servers.remove(i)
del remain[i]
else:
print('srv >> %r' % len(b))
i.send('y' * len(b))
if not want:
i.shutdown(socket.SHUT_WR)
elif i in clients:
b = i.recv(4096)
print('cli << %r' % len(b))
want = remain[i]
if want < len(b):
print('weird wanted %d bytes, got %d: %r' % (want, len(b), b))
assert(want >= len(b))
want -= len(b)
remain[i] = want
if not b: # EOF
if want:
print('weird: eof but wanted %d more' % want)
assert(want == 0)
i.close()
clients.remove(i)
del remain[i]
listener.accept()

View 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),
]

View 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

View 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()

View 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()

View 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()