././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/0000700000175000017500000000000000000000000012327 5ustar00brianbrian././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9785907 sshuttle-1.0.5/.github/0000700000175000017500000000000000000000000013667 5ustar00brianbrian././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/.github/workflows/0000700000175000017500000000000000000000000015724 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609113620.0 sshuttle-1.0.5/.github/workflows/pythonpackage.yml0000600000175000017500000000203700000000000021310 0ustar00brianbrian# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master, tproxy_mark_param ] pull_request: branches: [ master, tproxy_mark_param ] workflow_dispatch: branches: [ tproxy_mark_param ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-tests.txt - name: Lint with flake8 run: | flake8 sshuttle tests --count --show-source --statistics - name: Test with pytest run: | PYTHONPATH=$PWD pytest ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/.gitignore0000600000175000017500000000027400000000000014324 0ustar00brianbrian/sshuttle/version.py /tmp/ /.cache/ /.eggs/ /.tox/ /build/ /dist/ /sshuttle.egg-info/ /docs/_build/ *.pyc *~ *.8 /.do_built /.do_built.dir /.redo /.pytest_cache/ /.python-version .vscode/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1510815960.0 sshuttle-1.0.5/.prospector.yml0000600000175000017500000000062700000000000015337 0ustar00brianbrianstrictness: medium pylint: disable: - too-many-statements - too-many-locals - too-many-function-args - too-many-arguments - too-many-branches - bare-except - protected-access - no-else-return - unused-argument - method-hidden - arguments-differ - wrong-import-position - raising-bad-type pep8: options: max-line-length: 79 mccabe: run: false ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198487.0 sshuttle-1.0.5/CHANGES.rst0000600000175000017500000002077500000000000014146 0ustar00brianbrian========== Change log ========== All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog`_ and this project adheres to `Semantic Versioning`_. .. _`Keep a Changelog`: http://keepachangelog.com/ .. _`Semantic Versioning`: http://semver.org/ 1.0.5 - 2020-12-29 ------------------ Added ~~~~~ * IPv6 support in nft method. * Intercept DNS requests sent by systemd-resolved. * Set default tmark. * Fix python2 server compatibility. * Python 3.9 support. Fixed ~~~~~ * Change license text to LGPL-2.1 * Fix #494 sshuttle caught in infinite select() loop. * Include sshuttle version in verbose output. * Add psutil as dependency in setup.py * When subnets and excludes are specified with hostnames, use all IPs. * Update/document client's handling of IPv4 and IPv6. * Update sdnotify.py documentation. * Allow no remote to work. * Make prefixes in verbose output more consistent. * Make nat and nft rules consistent; improve rule ordering. * Make server and client handle resolv.conf differently. * Fix handling OSError in FirewallClient#__init__ * Refactor automatic method selection. Removed ~~~~~~~ * Drop testing of Python 3.5 1.0.4 - 2020-08-24 ------------------ Fixed ~~~~~ * Allow Mux() flush/fill to work with python < 3.5 * Fix parse_hostport to always return string for host. * Require -r/--remote parameter. * Add missing package in OpenWRT documentation. * Fix doc about --listen option. * README: add Ubuntu. * Increase IP4 ttl to 63 hops instead of 42. * Fix formatting in installation.rst 1.0.3 - 2020-07-12 ------------------ Fixed ~~~~~ * Ask setuptools to require Python 3.5 and above. * Add missing import. * Fix formatting typos in usage docs 1.0.2 - 2020-06-18 ------------------ Fixed ~~~~~ * Leave use of default port to ssh command. * Remove unwanted references to Python 2.7 in docs. * Replace usage of deprecated imp. * Fix connection with @ sign in username. 1.0.1 - 2020-06-05 ------------------ Fixed ~~~~~ * Errors in python long_documentation. 1.0.0 - 2020-06-05 ------------------ Added ~~~~~ * Python 3.8 support. * sshpass support. * Auto sudoers file (#269). * option for latency control buffer size. * Docs: FreeBSD'. * Docs: Nix'. * Docs: openwrt'. * Docs: install instructions for Fedora'. * Docs: install instructions for Arch Linux'. * Docs: 'My VPN broke and need a solution fast'. Removed ~~~~~~~ * Python 2.6 support. * Python 2.7 support. Fixed ~~~~~ * Remove debug message for getpeername failure. * Fix crash triggered by port scans closing socket. * Added "Running as a service" to docs. * Systemd integration. * Trap UnicodeError to handle cases where hostnames returned by DNS are invalid. * Formatting error in CHANGES.rst * Various errors in documentation. * Nftables based method. * Make hostwatch locale-independent (#379). * Add tproxy udp port mark filter that was missed in #144, fixes #367. * Capturing of local DNS servers. * Crashing on ECONNABORTED. * Size of pf_rule, which grew in OpenBSD 6.4. * Use prompt for sudo, not needed for doas. * Arch linux installation instructions. * tests for existing PR-312 (#337). * Hyphen in hostname. * Assembler import (#319). 0.78.5 - 2019-01-28 ------------------- Added ~~~~~ * doas support as replacmeent for sudo on OpenBSD. * Added ChromeOS section to documentation (#262) * Add --no-sudo-pythonpath option Fixed ~~~~~ * Fix forwarding to a single port. * Various updates to documentation. * Don't crash if we can't look up peername * Fix missing string formatting argument * Moved sshuttle/tests into tests. * Updated bandit config. * Replace path /dev/null by os.devnull. * Added coverage report to tests. * Fixes support for OpenBSD (6.1+) (#282). * Close stdin, stdout, and stderr when using syslog or forking to daemon (#283). * Changes pf exclusion rules precedence. * Fix deadlock with iptables with large ruleset. * docs: document --ns-hosts --to-ns and update --dns. * Use subprocess.check_output instead of run. * Fix potential deadlock condition in nft_get_handle. * auto-nets: retrieve routes only if using auto-nets. 0.78.4 - 2018-04-02 ------------------- Added ~~~~~ * Add homebrew instructions. * Route traffic by linux user. * Add nat-like method using nftables instead of iptables. Changed ~~~~~~~ * Talk to custom DNS server on pod, instead of the ones in /etc/resolv.conf. * Add new option for overriding destination DNS server. * Changed subnet parsing. Previously 10/8 become 10.0.0.0/8. Now it gets parsed as 0.0.0.10/8. * Make hostwatch find both fqdn and hostname. * Use versions of python3 greater than 3.5 when available (e.g. 3.6). Removed ~~~~~~~ * Remove Python 2.6 from automatic tests. Fixed ~~~~~ * Fix case where there is no --dns. * [pf] Avoid port forwarding from loopback address. * Use getaddrinfo to obtain a correct sockaddr. * Skip empty lines on incoming routes data. * Just skip empty lines of routes data instead of stopping processing. * [pf] Load pf kernel module when enabling pf. * [pf] Test double restore (ipv4, ipv6) disables only once; test kldload. * Fixes UDP and DNS proxies binding to the same socket address. * Mock socket bind to avoid depending on local IPs being available in test box. * Fix no value passed for argument auto_hosts in hw_main call. * Fixed incorrect license information in setup.py. * Preserve peer and port properly. * Make --to-dns and --ns-host work well together. * Remove test that fails under OSX. * Specify pip requirements for tests. * Use flake8 to find Python syntax errors or undefined names. * Fix compatibility with the sudoers file. * Stop using SO_REUSEADDR on sockets. * Declare 'verbosity' as global variable to placate linters. * Adds 'cd sshuttle' after 'git' to README and docs. * Documentation for loading options from configuration file. * Load options from a file. * Fix firewall.py. * Move sdnotify after setting up firewall rules. * Fix tests on Macos. 0.78.3 - 2017-07-09 ------------------- The "I should have done a git pull" first release. Fixed ~~~~~ * Order first by port range and only then by swidth 0.78.2 - 2017-07-09 ------------------- Added ~~~~~ * Adds support for tunneling specific port ranges (#144). * Add support for iproute2. * Allow remote hosts with colons in the username. * Re-introduce ipfw support for sshuttle on FreeBSD with support for --DNS option as well. * Add support for PfSense. * Tests and documentation for systemd integration. * Allow subnets to be given only by file (-s). Fixed ~~~~~ * Work around non tabular headers in BSD netstat. * Fix UDP and DNS support on Python 2.7 with tproxy method. * Fixed tests after adding support for iproute2. * Small refactoring of netstat/iproute parsing. * Set started_by_sshuttle False after disabling pf. * Fix punctuation and explain Type=notify. * Move pytest-runner to tests_require. * Fix warning: closed channel got=STOP_SENDING. * Support sdnotify for better systemd integration. * Fix #117 to allow for no subnets via file (-s). * Fix argument splitting for multi-word arguments. * requirements.rst: Fix mistakes. * Fix typo, space not required here. * Update installation instructions. * Support using run from different directory. * Ensure we update sshuttle/version.py in run. * Don't print python version in run. * Add CWD to PYTHONPATH in run. 0.78.1 - 2016-08-06 ------------------- * Fix readthedocs versioning. * Don't crash on ENETUNREACH. * Various bug fixes. * Improvements to BSD and OSX support. 0.78.0 - 2016-04-08 ------------------- * Don't force IPv6 if IPv6 nameservers supplied. Fixes #74. * Call /bin/sh as users shell may not be POSIX compliant. Fixes #77. * Use argparse for command line processing. Fixes #75. * Remove useless --server option. * Support multiple -s (subnet) options. Fixes #86. * Make server parts work with old versions of Python. Fixes #81. 0.77.2 - 2016-03-07 ------------------- * Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed. 0.77.1 - 2016-03-07 ------------------- * Use semantic versioning. http://semver.org/ * Update GPL 2 license text. * New release to fix PyPI. 0.77 - 2016-03-03 ----------------- * Various bug fixes. * Fix Documentation. * Add fix for MacOS X issue. * Add support for OpenBSD. 0.76 - 2016-01-17 ----------------- * Add option to disable IPv6 support. * Update documentation. * Move documentation, including man page, to Sphinx. * Use setuptools-scm for automatic versioning. 0.75 - 2016-01-12 ----------------- * Revert change that broke sshuttle entry point. 0.74 - 2016-01-10 ----------------- * Add CHANGES.rst file. * Numerous bug fixes. * Python 3.5 fixes. * PF fixes, especially for BSD. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1599604654.0 sshuttle-1.0.5/LICENSE0000600000175000017500000006364200000000000013351 0ustar00brianbrian GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1499555328.0 sshuttle-1.0.5/MANIFEST.in0000600000175000017500000000047300000000000014073 0ustar00brianbrianinclude *.txt include *.rst include *.py include MANIFEST.in include LICENSE include run include tox.ini exclude sshuttle/version.py recursive-include docs *.bat recursive-include docs *.py recursive-include docs *.rst recursive-include docs Makefile recursive-include sshuttle *.py recursive-exclude docs/_build * ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9865906 sshuttle-1.0.5/PKG-INFO0000600000175000017500000001017200000000000013427 0ustar00brianbrianMetadata-Version: 2.1 Name: sshuttle Version: 1.0.5 Summary: Full-featured" VPN over an SSH tunnel Home-page: https://github.com/sshuttle/sshuttle Author: Brian May Author-email: brian@linuxpenguins.xyz License: LGPL2.1+ Description: 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`_. .. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html Obtaining sshuttle ------------------ - Ubuntu 16.04 or later:: apt-get install sshuttle - Debian stretch or later:: apt-get install sshuttle - Arch Linux:: pacman -S sshuttle - Fedora:: dnf install sshuttle - NixOS:: nix-env -iA nixos.sshuttle - From PyPI:: sudo pip install sshuttle - Clone:: git clone https://github.com/sshuttle/sshuttle.git cd sshuttle sudo ./setup.py install - FreeBSD:: # ports cd /usr/ports/net/py-sshuttle && make install clean # pkg pkg install py36-sshuttle It is also possible to install into a virtualenv as a non-root user. - From PyPI:: virtualenv -p python3 /tmp/sshuttle . /tmp/sshuttle/bin/activate pip install sshuttle - Clone:: virtualenv -p python3 /tmp/sshuttle . /tmp/sshuttle/bin/activate git clone https://github.com/sshuttle/sshuttle.git cd sshuttle ./setup.py install - Homebrew:: brew install sshuttle - Nix:: nix-env -iA nixpkgs.sshuttle Documentation ------------- The documentation for the stable version is available at: https://sshuttle.readthedocs.org/ The documentation for the latest development version is available at: https://sshuttle.readthedocs.org/en/latest/ Running as a service -------------------- Sshuttle can also be run as a service and configured using a config management system: https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe Keywords: ssh vpn Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: System :: Networking Requires-Python: >=3.6 Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597269693.0 sshuttle-1.0.5/README.rst0000600000175000017500000000470000000000000014021 0ustar00brianbriansshuttle: 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`_. .. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html Obtaining sshuttle ------------------ - Ubuntu 16.04 or later:: apt-get install sshuttle - Debian stretch or later:: apt-get install sshuttle - Arch Linux:: pacman -S sshuttle - Fedora:: dnf install sshuttle - NixOS:: nix-env -iA nixos.sshuttle - From PyPI:: sudo pip install sshuttle - Clone:: git clone https://github.com/sshuttle/sshuttle.git cd sshuttle sudo ./setup.py install - FreeBSD:: # ports cd /usr/ports/net/py-sshuttle && make install clean # pkg pkg install py36-sshuttle It is also possible to install into a virtualenv as a non-root user. - From PyPI:: virtualenv -p python3 /tmp/sshuttle . /tmp/sshuttle/bin/activate pip install sshuttle - Clone:: virtualenv -p python3 /tmp/sshuttle . /tmp/sshuttle/bin/activate git clone https://github.com/sshuttle/sshuttle.git cd sshuttle ./setup.py install - Homebrew:: brew install sshuttle - Nix:: nix-env -iA nixpkgs.sshuttle Documentation ------------- The documentation for the stable version is available at: https://sshuttle.readthedocs.org/ The documentation for the latest development version is available at: https://sshuttle.readthedocs.org/en/latest/ Running as a service -------------------- Sshuttle can also be run as a service and configured using a config management system: https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1540157563.0 sshuttle-1.0.5/bandit.yml0000600000175000017500000000012500000000000014313 0ustar00brianbrianexclude_dirs: - tests skips: - B101 - B104 - B404 - B603 - B606 - B607 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/bin/0000700000175000017500000000000000000000000013077 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576185406.0 sshuttle-1.0.5/bin/sudoers-add0000700000175000017500000000314400000000000015241 0ustar00brianbrian#!/usr/bin/env bash # William Mantly # MIT License # https://github.com/wmantly/sudoers-add NEWLINE=$'\n' CONTENT="" ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then echo "Usage: $ME [file_path] [sudoers-file-name]" echo "Usage: [content] | $ME sudoers-file-name" echo "This will take a sudoers config validate it and add it to /etc/sudoers.d/{sudoers-file-name}" echo "The config can come from a file, first usage example or piped in second example." exit 0 fi if [ "$1" == "" ]; then (>&2 echo "This command take at lest one argument. See $ME --help") exit 1 fi if [ "$2" == "" ]; then FILE_NAME=$1 shift else FILE_NAME=$2 fi if [[ $EUID -ne 0 ]]; then echo "This script must be run as root" exit 1 fi while read -r line do CONTENT+="${line}${NEWLINE}" done < "${1:-/dev/stdin}" if [ "$CONTENT" == "" ]; then (>&2 echo "No config content specified. See $ME --help") exit 1 fi if [ "$FILE_NAME" == "" ]; then (>&2 echo "No sudoers file name specified. See $ME --help") exit 1 fi # Make a temp file to hold the sudoers config umask 077 TEMP_FILE=$(mktemp) echo "$CONTENT" > "$TEMP_FILE" # Make sure the content is valid visudo_STDOUT=$(visudo -c -f "$TEMP_FILE" 2>&1) visudo_code=$? # The temp file is no longer needed rm "$TEMP_FILE" if [ $visudo_code -eq 0 ]; then echo "$CONTENT" > "/etc/sudoers.d/$FILE_NAME" chmod 0440 "/etc/sudoers.d/$FILE_NAME" echo "The sudoers file /etc/sudoers.d/$FILE_NAME has been successfully created!" exit 0 else echo "Invalid sudoers config!" echo "$visudo_STDOUT" exit 1 fi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/docs/0000700000175000017500000000000000000000000013257 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/docs/Makefile0000600000175000017500000001516200000000000014726 0ustar00brianbrian# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sshuttle.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sshuttle.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/sshuttle" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sshuttle" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1499555328.0 sshuttle-1.0.5/docs/changes.rst0000600000175000017500000000003400000000000015420 0ustar00brianbrian.. include:: ../CHANGES.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1538872241.0 sshuttle-1.0.5/docs/chromeos.rst0000600000175000017500000000063100000000000015632 0ustar00brianbrianGoogle ChromeOS =============== Currently there is no built in support for running sshuttle directly on Google ChromeOS/Chromebooks. What we can really do is to create a Linux VM with Crostini. In the default stretch/Debian 9 VM, you can then install sshuttle as on any Linux box and it just works, as do xterms and ssvncviewer etc. https://www.reddit.com/r/Crostini/wiki/getstarted/crostini-setup-guide ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1499555328.0 sshuttle-1.0.5/docs/conf.py0000600000175000017500000002026600000000000014566 0ustar00brianbrian#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # sshuttle documentation build configuration file, created by # sphinx-quickstart on Sun Jan 17 12:13:47 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os sys.path.insert(0, os.path.abspath('..')) import sshuttle.version # NOQA # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.todo', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'sshuttle' copyright = '2016, Brian May' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = sshuttle.version.version # The short X.Y version. version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'sshuttledoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'sshuttle.tex', 'sshuttle documentation', 'Brian May', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('manpage', 'sshuttle', 'sshuttle documentation', ['Brian May'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'sshuttle', 'sshuttle documentation', 'Brian May', 'sshuttle', 'A transparent proxy-based VPN using ssh', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/docs/how-it-works.rst0000600000175000017500000000355000000000000016370 0ustar00brianbrianHow it works ============ sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind of both, and kind of neither. It's like a VPN, since it can forward every port on an entire network, not just ports you specify. Conveniently, it lets you use the "real" IP addresses of each host rather than faking port numbers on localhost. On the other hand, the way it *works* is more like ssh port forwarding than a VPN. Normally, a VPN forwards your data one packet at a time, and doesn't care about individual connections; ie. it's "stateless" with respect to the traffic. sshuttle is the opposite of stateless; it tracks every single connection. You could compare sshuttle to something like the old `Slirp `_ 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/docs/index.rst0000600000175000017500000000061500000000000015124 0ustar00brianbriansshuttle: where transparent proxy meets VPN meets ssh ===================================================== :Date: |today| :Version: |version| Contents: .. toctree:: :maxdepth: 2 overview requirements installation usage platform Man Page how-it-works support trivia changes Indices and tables ================== * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597269693.0 sshuttle-1.0.5/docs/installation.rst0000600000175000017500000000054300000000000016516 0ustar00brianbrianInstallation ============ - From PyPI:: pip install sshuttle - Debain package manager:: sudo apt install sshuttle - Clone:: git clone https://github.com/sshuttle/sshuttle.git cd sshuttle ./setup.py install Optionally after installation ----------------------------- - Add to sudoers file:: sshuttle --sudoers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/docs/make.bat0000600000175000017500000001506100000000000014671 0ustar00brianbrian@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sshuttle.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sshuttle.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/docs/manpage.rst0000600000175000017500000003636400000000000015437 0ustar00brianbriansshuttle ======== Synopsis -------- **sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\> Description ----------- :program:`sshuttle` allows you to create a VPN connection from your machine to any remote server that you can connect to via ssh, as long as that server has python 3.6 or higher. To work, you must have root access on the local machine, but you can have a normal account on the server. It's valid to run :program:`sshuttle` more than once simultaneously on a single client machine, connecting to a different server every time, so you can be on more than one VPN at once. If run on a router, :program:`sshuttle` can forward traffic for your entire subnet to the VPN. Options ------- .. program:: sshuttle .. option:: A list of subnets to route over the VPN, in the form ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4), 1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 netmask), and 0/0 ('just route everything through the VPN'). Any of the previous examples are also valid if you append a port or a port range, so 1.2.3.4:8000 will only tunnel traffic that has as the destination port 8000 of 1.2.3.4 and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet. A hostname can be provided instead of an IP address. If the hostname resolves to multiple IPs, all of the IPs are included. If a width is provided with a hostname that the width is applied to all of the hostnames IPs (if they are all either IPv4 or IPv6). Widths cannot be supplied to hostnames that resolve to both IPv4 and IPv6. Valid examples are example.com, example.com:8000, example.com/24, example.com/24:8000 and example.com:8000-9000. .. option:: --method Which firewall method should sshuttle use? For auto, sshuttle attempts to guess the appropriate method depending on what it can find in PATH. The default value is auto. .. option:: -l <[ip:]port>, --listen=<[ip:]port> Use this ip address and port number as the transparent proxy port. By default :program:`sshuttle` finds an available port automatically and listens on IP 127.0.0.1 (localhost), so you don't need to override it, and connections are only proxied from the local machine, not from outside machines. If you want to accept connections from other machines on your network (ie. to run :program:`sshuttle` on a router) try enabling IP Forwarding in your kernel, then using ``--listen 0.0.0.0:0``. You can use any name resolving to an IP address of the machine running :program:`sshuttle`, e.g. ``--listen localhost``. For the nft, tproxy and pf methods this can be an IPv6 address. Use this option with comma separated values if required, to provide both IPv4 and IPv6 addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``. .. option:: -H, --auto-hosts Scan for remote hostnames and update the local /etc/hosts file with matching entries for as long as the VPN is open. This is nicer than changing your system's DNS (/etc/resolv.conf) settings, for several reasons. First, hostnames are added without domain names attached, so you can ``ssh thatserver`` without worrying if your local domain matches the remote one. Second, if you :program:`sshuttle` into more than one VPN at a time, it's impossible to use more than one DNS server at once anyway, but :program:`sshuttle` correctly merges /etc/hosts entries between all running copies. Third, if you're only routing a few subnets over the VPN, you probably would prefer to keep using your local DNS server for everything else. .. option:: -N, --auto-nets In addition to the subnets provided on the command line, ask the server which subnets it thinks we should route, and route those automatically. The suggestions are taken automatically from the server's routing table. This feature does not detect IPv6 routes. Specify IPv6 subnets manually. For example, specify the ``::/0`` subnet on the command line to route all IPv6 traffic. .. option:: --dns Capture local DNS requests and forward to the remote DNS server. All queries to any of the local system's DNS servers (/etc/resolv.conf and, if it exists, /run/systemd/resolve/resolv.conf) will be intercepted and resolved on the remote side of the tunnel instead, there using the DNS specified via the :option:`--to-ns` option, if specified. Only plain DNS traffic sent to these servers on port 53 are captured. .. option:: --ns-hosts= Capture local DNS requests to the specified server(s) and forward to the remote DNS server. Contrary to the :option:`--dns` option, this flag allows to specify the DNS server(s) the queries to which to intercept, instead of intercepting all DNS traffic on the local machine. This can be useful when only certain DNS requests should be resolved on the remote side of the tunnel, e.g. in combination with dnsmasq. .. option:: --to-ns= The DNS to forward requests to when remote DNS resolution is enabled. If not given, sshuttle will simply resolve using the system configured resolver on the remote side (via /etc/resolv.conf on the remote side). .. option:: --python Specify the name/path of the remote python interpreter. The default is to use ``python3`` (or ``python``, if ``python3`` fails) in the remote system's PATH. .. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]> The remote hostname and optional username and ssh port number to use for connecting to the remote server. For example, example.com, testuser@example.com, testuser@example.com:2222, or example.com:2244. .. option:: -x , --exclude= Explicitly exclude this subnet from forwarding. The format of this option is the same as the ```` option. To exclude more than one subnet, specify the ``-x`` option more than once. You can say something like ``0/0 -x 1.2.3.0/24`` to forward everything except the local subnet over the VPN, for example. .. option:: -X , --exclude-from= Exclude the subnets specified in a file, one subnet per line. Useful when you have lots of subnets to exclude. .. option:: -v, --verbose Print more information about the session. This option can be used more than once for increased verbosity. By default, :program:`sshuttle` prints only error messages. .. option:: -e, --ssh-cmd The command to use to connect to the remote server. The default is just ``ssh``. Use this if your ssh client is in a non-standard location or you want to provide extra options to the ssh command, for example, ``-e 'ssh -v'``. .. option:: --seed-hosts A comma-separated list of hostnames to use to initialize the :option:`--auto-hosts` scan algorithm. :option:`--auto-hosts` does things like poll local SMB servers for lists of local hostnames, but can speed things up if you use this option to give it a few names to start from. If this option is used *without* :option:`--auto-hosts`, then the listed hostnames will be scanned and added, but no further hostnames will be added. .. option:: --no-latency-control Sacrifice latency to improve bandwidth benchmarks. ssh uses really big socket buffers, which can overload the connection if you start doing large file transfers, thus making all your other sessions inside the same tunnel go slowly. Normally, :program:`sshuttle` tries to avoid this problem using a "fullness check" that allows only a certain amount of outstanding data to be buffered at a time. But on high-bandwidth links, this can leave a lot of your bandwidth underutilized. It also makes :program:`sshuttle` seem slow in bandwidth benchmarks (benchmarks rarely test ping latency, which is what :program:`sshuttle` is trying to control). This option disables the latency control feature, maximizing bandwidth usage. Use at your own risk. .. option:: --latency-buffer-size Set the size of the buffer used in latency control. The default is ``32768``. Changing this option allows a compromise to be made between latency and bandwidth without completely disabling latency control (with :option:`--no-latency-control`). .. option:: -D, --daemon Automatically fork into the background after connecting to the remote server. Implies :option:`--syslog`. .. option:: -s , --subnets= Include the subnets specified in a file instead of on the command line. One subnet per line. .. option:: --syslog after connecting, send all log messages to the :manpage:`syslog(3)` service instead of stderr. This is implicit if you use :option:`--daemon`. .. option:: --pidfile= when using :option:`--daemon`, save :program:`sshuttle`'s pid to *pidfilename*. The default is ``sshuttle.pid`` in the current directory. .. option:: --disable-ipv6 Disable IPv6 support for methods that support it (nft, tproxy, and pf). .. option:: --firewall (internal use only) run the firewall manager. This is the only part of :program:`sshuttle` that must run as root. If you start :program:`sshuttle` as a non-root user, it will automatically run ``sudo`` or ``su`` to start the firewall manager, but the core of :program:`sshuttle` still runs as a normal user. .. option:: --hostwatch (internal use only) run the hostwatch daemon. This process runs on the server side and collects hostnames for the :option:`--auto-hosts` option. Using this option by itself makes it a lot easier to debug and test the :option:`--auto-hosts` feature. .. option:: --sudoers sshuttle will auto generate the proper sudoers.d config file and add it. Once this is completed, sshuttle will exit and tell the user if it succeed or not. Do not call this options with sudo, it may generate a incorrect config file. .. option:: --sudoers-no-modify sshuttle will auto generate the proper sudoers.d config and print it to stdout. The option will not modify the system at all. .. option:: --sudoers-user Set the user name or group with %group_name for passwordless operation. Default is the current user.set ALL for all users. Only works with --sudoers or --sudoers-no-modify option. .. option:: --sudoers-filename Set the file name for the sudoers.d file to be added. Default is "sshuttle_auto". Only works with --sudoers. .. option:: -t, --tmark Transproxy optional traffic mark with provided MARK value. .. option:: --version Print program version. Configuration File ------------------ All the options described above can optionally be specified in a configuration file. To run :program:`sshuttle` with options defined in, e.g., `/etc/sshuttle.conf` just pass the path to the file preceded by the `@` character, e.g. `@/etc/sshuttle.conf`. When running :program:`sshuttle` with options defined in a configuration file, options can still be passed via the command line in addition to what is defined in the file. If a given option is defined both in the file and in the command line, the value in the command line will take precedence. Arguments read from a file must be one per line, as shown below:: value --option1 value1 --option2 value2 Examples -------- Test locally by proxying all local connections, without using ssh:: $ sshuttle -v 0/0 Starting sshuttle proxy. Listening on ('0.0.0.0', 12300). [local sudo] Password: firewall manager ready. c : connecting to server... s: available routes: s: 192.168.42.0/24 c : connected. firewall manager: starting transproxy. c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139. c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443. ...etc... ^C firewall manager: undoing changes. KeyboardInterrupt c : Keyboard interrupt: exiting. c : SW#8:192.168.42.121:47523: deleting c : SW#6:192.168.42.106:50035: deleting Test connection to a remote server, with automatic hostname and subnet guessing:: $ sshuttle -vNHr example.org Starting sshuttle proxy. Listening on ('0.0.0.0', 12300). firewall manager ready. c : connecting to server... s: available routes: s: 77.141.99.0/24 c : connected. c : seed_hosts: [] firewall manager: starting transproxy. hostwatch: Found: testbox1: 1.2.3.4 hostwatch: Found: mytest2: 5.6.7.8 hostwatch: Found: domaincontroller: 99.1.2.3 c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22. ^C firewall manager: undoing changes. c : Keyboard interrupt: exiting. c : SW#6:192.168.42.121:60554: deleting Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file:: $ sshuttle @/etc/sshuttle.conf Use the options defined in `/etc/sshuttle.conf` but be more verbose:: $ sshuttle @/etc/sshuttle.conf -vvv Override the remote server defined in `/etc/sshuttle.conf`:: $ sshuttle @/etc/sshuttle.conf -r otheruser@test.example.com Example configuration file:: 192.168.0.0/16 --remote user@example.com Discussion ---------- When it starts, :program:`sshuttle` creates an ssh session to the server specified by the ``-r`` option. If ``-r`` is omitted, it will start both its client and server locally, which is sometimes useful for testing. After connecting to the remote server, :program:`sshuttle` uploads its (python) source code to the remote end and executes it there. Thus, you don't need to install :program:`sshuttle` on the remote server, and there are never :program:`sshuttle` version conflicts between client and server. Unlike most VPNs, :program:`sshuttle` forwards sessions, not packets. That is, it uses kernel transparent proxying (`iptables REDIRECT` rules on Linux) to capture outgoing TCP sessions, then creates entirely separate TCP sessions out to the original destination at the other end of the tunnel. Packet-level forwarding (eg. using the tun/tap devices on Linux) seems elegant at first, but it results in several problems, notably the 'tcp over tcp' problem. The tcp protocol depends fundamentally on packets being dropped in order to implement its congestion control agorithm; if you pass tcp packets through a tcp-based tunnel (such as ssh), the inner tcp packets will never be dropped, and so the inner tcp stream's congestion control will be completely broken, and performance will be terrible. Thus, packet-based VPNs (such as IPsec and openvpn) cannot use tcp-based encrypted streams like ssh or ssl, and have to implement their own encryption from scratch, which is very complex and error prone. :program:`sshuttle`'s simplicity comes from the fact that it can safely use the existing ssh encrypted tunnel without incurring a performance penalty. It does this by letting the client-side kernel manage the incoming tcp stream, and the server-side kernel manage the outgoing tcp stream; there is no need for congestion control to be shared between the two separate streams, so a tcp-based tunnel is fine. .. seealso:: :manpage:`ssh(1)`, :manpage:`python(1)` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597269693.0 sshuttle-1.0.5/docs/openwrt.rst0000600000175000017500000000033100000000000015506 0ustar00brianbrianOpenWRT ======== Run:: opkg install python3 python3-pip iptables-mod-extra iptables-mod-nat-extra iptables-mod-ipopt python3 /usr/bin/pip3 install sshuttle sshuttle -l 0.0.0.0 -r -x 192.168.1.1 0/0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1498514474.0 sshuttle-1.0.5/docs/overview.rst0000600000175000017500000000151700000000000015665 0ustar00brianbrianOverview ======== As far as I know, sshuttle is the only program that solves the following common case: - Your client machine (or router) is Linux, MacOS, FreeBSD, OpenBSD or pfSense. - You have access to a remote network via ssh. - You don't necessarily have admin access on the remote network. - The remote network has no VPN, or only stupid/complex VPN protocols (IPsec, PPTP, etc). Or maybe you *are* the admin and you just got frustrated with the awful state of VPN tools. - You don't want to create an ssh port forward for every single host/port on the remote network. - You hate openssh's port forwarding because it's randomly slow and/or stupid. - You can't use openssh's PermitTunnel feature because it's disabled by default on openssh servers; plus it does TCP-over-TCP, which has terrible performance (see below). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1581237852.0 sshuttle-1.0.5/docs/platform.rst0000600000175000017500000000020600000000000015635 0ustar00brianbrianPlatform Specific Notes ======================= Contents: .. toctree:: :maxdepth: 2 chromeos tproxy windows openwrt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109795.0 sshuttle-1.0.5/docs/requirements.rst0000600000175000017500000000335000000000000016537 0ustar00brianbrianRequirements ============ Client side Requirements ------------------------ - sudo, or root access on your client machine. (The server doesn't need admin access.) - Python 3.6 or greater. Linux with NAT method ~~~~~~~~~~~~~~~~~~~~~ Supports: * IPv4 TCP * IPv4 DNS Requires: * iptables DNAT, REDIRECT, and ttl modules. Linux with nft method ~~~~~~~~~~~~~~~~~~~~~ Supports * IPv4 TCP * IPv4 DNS * IPv6 TCP * IPv6 DNS Requires: * nftables Linux with TPROXY method ~~~~~~~~~~~~~~~~~~~~~~~~ Supports: * IPv4 TCP * IPv4 UDP (requires ``recvmsg`` - see below) * IPv6 DNS (requires ``recvmsg`` - see below) * IPv6 TCP * IPv6 UDP (requires ``recvmsg`` - see below) * IPv6 DNS (requires ``recvmsg`` - see below) MacOS / FreeBSD / OpenBSD / pfSense ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Method: pf Supports: * IPv4 TCP * IPv4 DNS * IPv6 TCP * IPv6 DNS Requires: * You need to have the pfctl command. Windows ~~~~~~~ Not officially supported, however can be made to work with Vagrant. Requires cmd.exe with Administrator access. See :doc:`windows` for more information. Server side Requirements ------------------------ - Python 3.6 or greater. Additional Suggested Software ----------------------------- - If you are using systemd, sshuttle can notify it when the connection to the remote end is established and the firewall rules are installed. For this feature to work you must configure the process start-up type for the sshuttle service unit to notify, as shown in the example below. .. code-block:: ini :emphasize-lines: 6 [Unit] Description=sshuttle After=network.target [Service] Type=notify ExecStart=/usr/bin/sshuttle --dns --remote @ [Install] WantedBy=multi-user.target ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/docs/support.rst0000600000175000017500000000037600000000000015535 0ustar00brianbrianSupport ======= Mailing list: * Subscribe by sending a message to * List archives are at: http://groups.google.com/group/sshuttle Issue tracker and pull requests at github: * https://github.com/sshuttle/sshuttle ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/docs/tproxy.rst0000600000175000017500000000347000000000000015364 0ustar00brianbrianTPROXY ====== TPROXY is the only method that has full support of IPv6 and UDP. There are some things you need to consider for TPROXY to work: - The following commands need to be run first as root. This only needs to be done once after booting up:: ip route add local default dev lo table 100 ip rule add fwmark {TMARK} lookup 100 ip -6 route add local default dev lo table 100 ip -6 rule add fwmark {TMARK} lookup 100 where {TMARK} is the identifier mark passed with -t or --tmark flag (default value is 1). - The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6 routes manually. e.g. by adding ``'::/0'`` to the end of the command line. - The client needs to be run as root. e.g.:: sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ... - You may need to exclude the IP address of the server you are connecting to. Otherwise sshuttle may attempt to intercept the ssh packets, which will not work. Use the ``--exclude`` parameter for this. - Similarly, UDP return packets (including DNS) could get intercepted and bounced back. This is the case if you have a broad subnet such as ``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the ``--exclude`` parameter for this. - You need the ``--method=tproxy`` parameter, as above. - The routes for the outgoing packets must already exist. For example, if your connection does not have IPv6 support, no IPv6 routes will exist, IPv6 packets will not be generated and sshuttle cannot intercept them:: telnet -6 www.google.com 80 Trying 2404:6800:4001:805::1010... telnet: Unable to connect to remote host: Network is unreachable Add some dummy routes to external interfaces. Make sure they get removed however after sshuttle exits. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/docs/trivia.rst0000600000175000017500000000362300000000000015315 0ustar00brianbrianUseless Trivia ============== This section written by the original author, Avery Pennarun . Back in 1998, I released the first version of `Tunnel Vision `_, 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 `_. He asked me to do a contract for him to build an initial prototype of a transparent proxy server for mobile networks. The idea was similar to sshuttle: if you reassemble and then disassemble the TCP packets, you can reduce latency and improve performance vs. just forwarding the packets over a plain VPN or mobile network. (It's unlikely that any of my code has persisted in the Slipstream product today, but the concept is still pretty cool. I'm still horrified that people use plain TCP on complex mobile networks with crazily variable latency, for which it was never really intended.) That project I did for Slipstream was what first gave me the idea to merge the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single program that was the best of all worlds. And here we are, at last. You're welcome. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1594544797.0 sshuttle-1.0.5/docs/usage.rst0000600000175000017500000001036500000000000015124 0ustar00brianbrianUsage ===== .. note:: For information on usage with Windows, see the :doc:`windows` section. For information on using the TProxy method, see the :doc:`tproxy` section. Forward all traffic:: sshuttle -r username@sshserver 0.0.0.0/0 - Use the :option:`sshuttle -r` parameter to specify a remote server. - By default sshuttle will automatically choose a method to use. Override with the :option:`sshuttle --method` parameter. - There is a shortcut for 0.0.0.0/0 for those that value their wrists:: sshuttle -r username@sshserver 0/0 - For 'My VPN broke and need a temporary solution FAST to access local IPv4 addresses':: sshuttle --dns -NHr username@sshserver 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 If you would also like your DNS queries to be proxied through the DNS server of the server you are connect to:: sshuttle --dns -r username@sshserver 0/0 The above is probably what you want to use to prevent local network attacks such as Firesheep and friends. See the documentation for the :option:`sshuttle --dns` parameter. (You may be prompted for one or more passwords; first, the local password to become root using sudo, and then the remote ssh password. Or you might have sudo and ssh set up to not require passwords, in which case you won't be prompted at all.) Usage Notes ----------- That's it! Now your local machine can access the remote network as if you were right there. And if your "client" machine is a router, everyone on your local network can make connections to your remote network. You don't need to install sshuttle on the remote server; the remote server just needs to have python available. sshuttle will automatically upload and run its source code to the remote python interpreter. This creates a transparent proxy server on your local machine for all IP addresses that match 0.0.0.0/0. (You can use more specific IP addresses if you want; use any number of IP addresses or subnets to change which addresses get proxied. Using 0.0.0.0/0 proxies *everything*, which is interesting if you don't trust the people on your local network.) Any TCP session you initiate to one of the proxied IP addresses will be captured by sshuttle and sent over an ssh session to the remote copy of sshuttle, which will then regenerate the connection on that end, and funnel the data back and forth through ssh. Fun, right? A poor man's instant VPN, and you don't even have to have admin access on the server. Sudoers File ------------ sshuttle can auto-generate the proper sudoers.d file using the current user for Linux and OSX. Doing this will allow sshuttle to run without asking for the local sudo password and to give users who do not have sudo access ability to run sshuttle:: sshuttle --sudoers DO NOT run this command with sudo, it will ask for your sudo password when it is needed. A costume user or group can be set with the : option:`sshuttle --sudoers --sudoers-username {user_descriptor}` option. Valid values for this vary based on how your system is configured. Values such as usernames, groups pre-pended with `%` and sudoers user aliases will work. See the sudoers manual for more information on valid user specif actions. The options must be used with `--sudoers`:: sshuttle --sudoers --sudoers-user mike sshuttle --sudoers --sudoers-user %sudo The name of the file to be added to sudoers.d can be configured as well. This is mostly not necessary but can be useful for giving more than one user access to sshuttle. The default is `sshuttle_auto`:: sshuttle --sudoer --sudoers-filename sshuttle_auto_mike sshuttle --sudoer --sudoers-filename sshuttle_auto_tommy You can also see what configuration will be added to your system without modifying anything. This can be helpfull is the auto feature does not work, or you want more control. This option also works with `--sudoers-username`. `--sudoers-filename` has no effect with this option:: sshuttle --sudoers-no-modify This will simply sprint the generated configuration to STDOUT. Example:: 08:40 PM william$ sshuttle --sudoers-no-modify Cmnd_Alias SSHUTTLE304 = /usr/bin/env PYTHONPATH=/usr/local/lib/python2.7/dist-packages/sshuttle-0.78.5.dev30+gba5e6b5.d20180909-py2.7.egg /usr/bin/python /usr/local/bin/sshuttle --method auto --firewall william ALL=NOPASSWD: SSHUTTLE304 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/docs/windows.rst0000600000175000017500000000145100000000000015506 0ustar00brianbrianMicrosoft Windows ================= Currently there is no built in support for running sshuttle directly on Microsoft Windows. What we can really do is to create a Linux VM with Vagrant (or simply Virtualbox if you like). In the Vagrant settings, remember to turn on bridged NIC. Then, run sshuttle inside the VM like below:: sshuttle -l 0.0.0.0 -x 10.0.0.0/8 -x 192.168.0.0/16 0/0 10.0.0.0/8 excludes NAT traffic of Vagrant and 192.168.0.0/16 excludes traffic to local area network (assuming that we're using 192.168.0.0 subnet). Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can configure that in Vagrant), we can then ask Windows to route all its traffic via the VM by running the following in cmd.exe with admin right:: route add 0.0.0.0 mask 0.0.0.0 192.168.1.200 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608110160.0 sshuttle-1.0.5/requirements-tests.txt0000600000175000017500000000015500000000000016756 0ustar00brianbrian-r requirements.txt attrs==20.3.0 pytest==6.2.1 pytest-cov==2.10.1 mock==4.0.3 flake8==3.8.4 pyflakes==2.2.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/requirements.txt0000600000175000017500000000003500000000000015613 0ustar00brianbriansetuptools-scm==5.0.1 psutil ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1590702310.0 sshuttle-1.0.5/run0000700000175000017500000000053300000000000013062 0ustar00brianbrian#!/usr/bin/env sh set -e export PYTHONPATH="$(dirname $0):$PYTHONPATH" export PATH="$(dirname $0)/bin:$PATH" python_best_version() { if [ -x "$(command -v python3)" ] && python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then exec python3 "$@" else exec python "$@" fi } python_best_version -m "sshuttle" "$@" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9865906 sshuttle-1.0.5/setup.cfg0000600000175000017500000000043300000000000014152 0ustar00brianbrian[aliases] test = pytest [bdist_wheel] universal = 1 [upload] sign = true identity = 0x1784577F811F6EAC [flake8] count = true show-source = true statistics = true [tool:pytest] addopts = --cov=sshuttle --cov-branch --cov-report=term-missing [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609113580.0 sshuttle-1.0.5/setup.py0000700000175000017500000000456500000000000014056 0ustar00brianbrian#!/usr/bin/env python # Copyright 2012-2014 Brian May # # This file is part of sshuttle. # # sshuttle is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 2.1 of # the License, or (at your option) any later version. # # sshuttle 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with sshuttle; If not, see . from setuptools import setup, find_packages def version_scheme(version): from setuptools_scm.version import guess_next_dev_version version = guess_next_dev_version(version) return version.lstrip("v") setup( name="sshuttle", use_scm_version={ 'write_to': "sshuttle/version.py", 'version_scheme': version_scheme, }, setup_requires=['setuptools_scm'], # version=version, url='https://github.com/sshuttle/sshuttle', author='Brian May', author_email='brian@linuxpenguins.xyz', description='Full-featured" VPN over an SSH tunnel', packages=find_packages(), license="LGPL2.1+", long_description=open('README.rst').read(), long_description_content_type="text/x-rst", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: " "GNU Lesser General Public License v2 or later (LGPLv2+)", "Operating System :: OS Independent", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: System :: Networking", ], scripts=['bin/sudoers-add'], entry_points={ 'console_scripts': [ 'sshuttle = sshuttle.cmdline:main', ], }, python_requires='>=3.6', install_requires=[ 'psutil', ], tests_require=[ 'pytest', 'pytest-cov', 'pytest-runner', 'mock', 'flake8', ], keywords="ssh vpn", ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/sshuttle/0000700000175000017500000000000000000000000014202 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109731.0 sshuttle-1.0.5/sshuttle/__init__.py0000600000175000017500000000015500000000000016316 0ustar00brianbriantry: from sshuttle.version import version as __version__ except ImportError: __version__ = "unknown" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1487571182.0 sshuttle-1.0.5/sshuttle/__main__.py0000600000175000017500000000014400000000000016275 0ustar00brianbrian"""Coverage.py's main entry point.""" import sys from sshuttle.cmdline import main sys.exit(main()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/sshuttle/assembler.py0000600000175000017500000000271600000000000016541 0ustar00brianbrianimport sys import zlib import types verbosity = verbosity # noqa: F821 must be a previously defined global z = zlib.decompressobj() while 1: name = sys.stdin.readline().strip() if name: # python2 compat: in python2 sys.stdin.readline().strip() -> str # in python3 sys.stdin.readline().strip() -> bytes # (see #481) if sys.version_info >= (3, 0): name = name.decode("ASCII") nbytes = int(sys.stdin.readline()) if verbosity >= 2: sys.stderr.write(' s: assembling %r (%d bytes)\n' % (name, nbytes)) content = z.decompress(sys.stdin.read(nbytes)) module = types.ModuleType(name) parents = name.rsplit(".", 1) if len(parents) == 2: parent, parent_name = parents setattr(sys.modules[parent], parent_name, module) code = compile(content, name, "exec") exec(code, module.__dict__) # nosec sys.modules[name] = module else: break sys.stderr.flush() sys.stdout.flush() # import can only happen once the code has been transferred to # the server. 'noqa: E402' excludes these lines from QA checks. import sshuttle.helpers # noqa: E402 sshuttle.helpers.verbose = verbosity import sshuttle.cmdline_options as options # noqa: E402 from sshuttle.server import main # noqa: E402 main(options.latency_control, options.auto_hosts, options.to_nameserver, options.auto_nets) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/client.py0000600000175000017500000007463100000000000016047 0ustar00brianbrianimport errno import re import signal import time import subprocess as ssubprocess import os import sys import platform import psutil import sshuttle.helpers as helpers import sshuttle.ssnet as ssnet import sshuttle.ssh as ssh import sshuttle.ssyslog as ssyslog import sshuttle.sdnotify as sdnotify from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \ resolvconf_nameservers, which from sshuttle.methods import get_method, Features from sshuttle import __version__ try: from pwd import getpwnam except ImportError: getpwnam = None try: # try getting recvmsg from python import socket as pythonsocket getattr(pythonsocket.socket, "recvmsg") socket = pythonsocket except AttributeError: # try getting recvmsg from socket_ext library try: import socket_ext getattr(socket_ext.socket, "recvmsg") socket = socket_ext except ImportError: import socket _extra_fd = os.open(os.devnull, 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("c : 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, b'%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(os.devnull, 'r+') os.dup2(si.fileno(), 0) os.dup2(si.fileno(), 1) si.close() def daemon_cleanup(): try: os.unlink(_pidname) except OSError as e: if e.errno == errno.ENOENT: pass else: raise class MultiListener: def __init__(self, kind=socket.SOCK_STREAM, proto=0): self.type = kind self.proto = proto self.v6 = None self.v4 = None self.bind_called = False def setsockopt(self, level, optname, value): assert(self.bind_called) 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): assert(self.bind_called) 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): assert(self.bind_called) 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): assert(not self.bind_called) self.bind_called = True if address_v6 is not None: self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto) self.v6.bind(address_v6) else: self.v6 = None if address_v4 is not None: self.v4 = socket.socket(socket.AF_INET, self.type, self.proto) self.v4.bind(address_v4) else: self.v4 = None def print_listening(self, what): assert(self.bind_called) if self.v6: listenip = self.v6.getsockname() debug1('%s listening on %r.\n' % (what, listenip)) debug2('%s listening with %r.\n' % (what, self.v6)) 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, sudo_pythonpath): self.auto_nets = [] python_path = os.path.dirname(os.path.dirname(__file__)) argvbase = ([sys.executable, sys.argv[0]] + ['-v'] * (helpers.verbose or 0) + ['--method', method_name] + ['--firewall']) if ssyslog._p: argvbase += ['--syslog'] # Determine how to prefix the command in order to elevate privileges. if platform.platform().startswith('OpenBSD'): elev_prefix = ['doas'] # OpenBSD uses built in `doas` else: elev_prefix = ['sudo', '-p', '[local sudo] Password: '] # Look for binary and switch to absolute path if we can find # it. path = which(elev_prefix[0]) if path: elev_prefix[0] = path if sudo_pythonpath: elev_prefix += ['/usr/bin/env', 'PYTHONPATH=%s' % python_path] argv_tries = [elev_prefix + argvbase, argvbase] # we can't use stdin/stdout=subprocess.PIPE here, as we normally would, # 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() 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) # No env: Talking to `FirewallClient.start`, which has no i18n. break except OSError as e: log('Spawning firewall manager: %r\n' % argv) raise Fatal(e) self.argv = argv s1.close() self.pfile = s2.makefile('rwb') 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, user, tmark): 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 self.user = user self.tmark = tmark 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, fport, lport) \ in self.subnets_include + self.auto_nets: self.pfile.write(b'%d,%d,0,%s,%d,%d\n' % (family, width, ip.encode("ASCII"), fport, lport)) for (family, ip, width, fport, lport) in self.subnets_exclude: self.pfile.write(b'%d,%d,1,%s,%d,%d\n' % (family, width, ip.encode("ASCII"), fport, lport)) self.pfile.write(b'NSLIST\n') 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 if self.user is None: user = b'-' elif isinstance(self.user, str): user = bytes(self.user, 'utf-8') else: user = b'%d' % self.user self.pfile.write(b'GO %d %s\n' % (udp, user)) 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(br'[^-\w\.]', hostname)) assert(not re.search(br'[^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): remove = [] for chan, timeout in dnsreqs.items(): if timeout < now: debug3('expiring dnsreqs channel=%d\n' % chan) remove.append(chan) del mux.channels[chan] for chan in remove: del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) remove = [] for peer, (chan, timeout) in udp_by_src.items(): if timeout < now: debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer)) mux.send(chan, ssnet.CMD_UDP_CLOSE, b'') remove.append(peer) del mux.channels[chan] for peer in remove: del udp_by_src[peer] debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) 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(os.devnull, os.O_RDONLY) return else: raise dstip = method.get_tcp_dstip(sock) debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1], 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(b",", 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, _ = 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, b"%d" % listener.family) udp_by_src[srcip] = chan, now + 30 hdr = b"%s,%d," % (dstip[0].encode("ASCII"), 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 # dstip is None if we are using a method where we can't determine # the destination IP of the DNS request that we captured from the client. if dstip is None: debug1('DNS request from %r: %d bytes\n' % (srcip, len(data))) else: 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_hosts, auto_nets, daemon, to_nameserver): helpers.logprefix = 'c : ' debug1('Starting client with Python version %s\n' % platform.python_version()) method = fw.method handlers = [] 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, auto_hosts=auto_hosts, to_nameserver=to_nameserver, auto_nets=auto_nets)) except socket.error as e: if e.args[0] == errno.EPIPE: raise Fatal("c : failed to establish ssh session (1)") else: raise mux = Mux(serversock.makefile("rb"), serversock.makefile("wb")) 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("c : failed to establish ssh session (2)") else: raise rv = serverproc.poll() if rv: raise Fatal('c : server died with error code %d' % rv) if initstring != expected: raise Fatal('c : expected server init string %r; got %r' % (expected, initstring)) log('Connected to server.\n') sys.stdout.flush() if daemon: daemonize() log('daemonizing (%s).\n' % _pidname) def onroutes(routestr): if auto_nets: for line in routestr.strip().split(b'\n'): if not line: continue (family, ip, width) = line.split(b',', 2) family = int(family) width = int(width) ip = ip.decode("ASCII") if family == socket.AF_INET6 and tcp_listener.v6 is None: debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width)) if family == socket.AF_INET and tcp_listener.v4 is None: debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width)) else: debug2("Adding auto net %d/%s/%d\n" % (family, ip, width)) fw.auto_nets.append((family, ip, width, 0, 0)) # we definitely want to do this *after* starting ssh, or we might end # 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 serverready() mux.got_routes = onroutes def serverready(): fw.start() sdnotify.send(sdnotify.ready(), sdnotify.status('Connected')) def onhostlist(hostlist): debug2('got host list: %r\n' % hostlist) for line in hostlist.strip().split(): if line: name, ip = line.split(b',', 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, str.encode('\n'.join(seed_hosts))) def check_ssh_alive(): if daemon: # poll() won't tell us when process exited since the # process is no longer our child (it returns 0 all the # time). if not psutil.pid_exists(serverproc.pid): raise Fatal('ssh connection to server (pid %d) exited.' % serverproc.pid) else: rv = serverproc.poll() # poll returns None if process hasn't exited. if rv is not None: raise Fatal('ssh connection to server (pid %d) exited ' 'with returncode %d' % (serverproc.pid, rv)) while 1: check_ssh_alive() ssnet.runonce(handlers, mux) if latency_control: mux.check_fullness() def main(listenip_v6, listenip_v4, ssh_cmd, remotename, python, latency_control, dns, nslist, method_name, seed_hosts, auto_hosts, auto_nets, subnets_include, subnets_exclude, daemon, to_nameserver, pidfile, user, sudo_pythonpath, tmark): if not remotename: print("WARNING: You must specify -r/--remote to securely route " "traffic to a remote machine. Running without -r/--remote " "is only recommended for testing.") if daemon: try: check_daemon(pidfile) except Fatal as e: log("%s\n" % e) return 5 debug1('Starting sshuttle proxy (version %s).\n' % __version__) helpers.logprefix = 'c : ' fw = FirewallClient(method_name, sudo_pythonpath) # If --dns is used, store the IP addresses that the client # normally uses for DNS lookups in nslist. The firewall needs to # redirect packets outgoing to this server to the remote host # instead. if dns: nslist += resolvconf_nameservers(True) if to_nameserver is not None: to_nameserver = "%s@%s" % tuple(to_nameserver[1:]) else: # option doesn't make sense if we aren't proxying dns if to_nameserver and len(to_nameserver) > 0: print("WARNING: --to-ns option is ignored because --dns was not " "used.") to_nameserver = None # Get family specific subnet lists. Also, the user may not specify # any subnets if they use --auto-nets. In this case, our subnets # list will be empty and the forwarded subnets will be determined # later by the server. subnets_v4 = [i for i in subnets_include if i[0] == socket.AF_INET] subnets_v6 = [i for i in subnets_include if i[0] == socket.AF_INET6] nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] # Get available features from the firewall method avail = fw.method.get_supported_features() # A feature is "required" if the user supplies us parameters which # implies that the feature is needed. required = Features() # Select the default addresses to bind to / listen to. # Assume IPv4 is always available and should always be enabled. If # a method doesn't provide IPv4 support or if we wish to run # ipv6-only, changes to this code are required. assert avail.ipv4 required.ipv4 = True # listenip_v4 contains user specified value or it is set to "auto". if listenip_v4 == "auto": listenip_v4 = ('127.0.0.1', 0) # listenip_v6 is... # None when IPv6 is disabled. # "auto" when listen address is unspecified. # The user specified address if provided by user if listenip_v6 is None: debug1("IPv6 disabled by --disable-ipv6\n") if listenip_v6 == "auto": if avail.ipv6: debug1("IPv6 enabled: Using default IPv6 listen address ::1\n") listenip_v6 = ('::1', 0) else: debug1("IPv6 disabled since it isn't supported by method " "%s.\n" % fw.method.name) listenip_v6 = None # Make final decision about enabling IPv6: required.ipv6 = False if listenip_v6: required.ipv6 = True # If we get here, it is possible that listenip_v6 was user # specified but not supported by the current method. if required.ipv6 and not avail.ipv6: raise Fatal("An IPv6 listen address was supplied, but IPv6 is " "disabled at your request or is unsupported by the %s " "method." % fw.method.name) if user is not None: if getpwnam is None: raise Fatal("Routing by user not available on this system.") try: user = getpwnam(user).pw_uid except KeyError: raise Fatal("User %s does not exist." % user) required.user = False if user is None else True if not required.ipv6 and len(subnets_v6) > 0: print("WARNING: IPv6 subnets were ignored because IPv6 is disabled " "in sshuttle.") subnets_v6 = [] subnets_include = subnets_v4 required.udp = avail.udp # automatically enable UDP if it is available required.dns = len(nslist) > 0 # Remove DNS servers using IPv6. if required.dns: if not required.ipv6 and len(nslist_v6) > 0: print("WARNING: Your system is configured to use an IPv6 DNS " "server but sshuttle is not using IPv6. Therefore DNS " "traffic your system sends to the IPv6 DNS server won't " "be redirected via sshuttle to the remote machine.") nslist_v6 = [] nslist = nslist_v4 if len(nslist) == 0: raise Fatal("Can't redirect DNS traffic since IPv6 is not " "enabled in sshuttle and all of the system DNS " "servers are IPv6.") # If we aren't using IPv6, we can safely ignore excluded IPv6 subnets. if not required.ipv6: orig_len = len(subnets_exclude) subnets_exclude = [i for i in subnets_exclude if i[0] == socket.AF_INET] if len(subnets_exclude) < orig_len: print("WARNING: Ignoring one or more excluded IPv6 subnets " "because IPv6 is not enabled.") # This will print error messages if we required a feature that # isn't available by the current method. fw.method.assert_features(required) # display features enabled def feature_status(label, enabled, available): msg = label + ": " if enabled: msg += "on" else: msg += "off " if available: msg += "(available)" else: msg += "(not available with %s method)" % fw.method.name debug1(msg + "\n") debug1("Method: %s\n" % fw.method.name) feature_status("IPv4", required.ipv4, avail.ipv4) feature_status("IPv6", required.ipv6, avail.ipv6) feature_status("UDP ", required.udp, avail.udp) feature_status("DNS ", required.dns, avail.dns) feature_status("User", required.user, avail.user) # Exclude traffic destined to our listen addresses. if required.ipv4 and \ not any(listenip_v4[0] == sex[1] for sex in subnets_v4): subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0)) if required.ipv6 and \ not any(listenip_v6[0] == sex[1] for sex in subnets_v6): subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0)) # We don't print the IP+port of where we are listening here # because we do that below when we have identified the ports to # listen on. debug1("Subnets to forward through remote host (type, IP, cidr mask " "width, startPort, endPort):\n") for i in subnets_include: debug1(" "+str(i)+"\n") if auto_nets: debug1("NOTE: Additional subnets to forward may be added below by " "--auto-nets.\n") debug1("Subnets to exclude from forwarding:\n") for i in subnets_exclude: debug1(" "+str(i)+"\n") if required.dns: debug1("DNS requests normally directed at these servers will be " "redirected to remote:\n") for i in nslist: debug1(" "+str(i)+"\n") 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) # keep track of failed bindings and used ports to avoid trying to # bind to the same socket address twice in different listeners used_ports = [] # search for free ports and try to bind last_e = None redirectport_v6 = 0 redirectport_v4 = 0 bound = False for port in ports: debug2('Trying to bind redirector on port %d\n' % port) tcp_listener = MultiListener() if required.udp: udp_listener = MultiListener(socket.SOCK_DGRAM) 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 used_ports.append(port) break except socket.error as e: if e.errno == errno.EADDRINUSE: last_e = e used_ports.append(port) else: raise e 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 required.dns: # search for spare port for DNS ports = range(12300, 9000, -1) for port in ports: debug2('Trying to bind DNS redirector on port %d\n' % port) if port in used_ports: continue 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 used_ports.append(port) break except socket.error as e: if e.errno == errno.EADDRINUSE: last_e = e used_ports.append(port) else: raise e dns_listener.print_listening("DNS") if not bound: assert(last_e) raise last_e else: dnsport_v6 = 0 dnsport_v4 = 0 dns_listener = None # Last minute sanity checks. # These should never fail. # If these do fail, something is broken above. if subnets_v6: assert required.ipv6 if redirectport_v6 == 0: raise Fatal("IPv6 subnets defined but not listening") if nslist_v6: assert required.dns assert required.ipv6 if dnsport_v6 == 0: raise Fatal("IPv6 ns servers defined but not listening") if subnets_v4: if redirectport_v4 == 0: raise Fatal("IPv4 subnets defined but not listening") if nslist_v4: if dnsport_v4 == 0: raise Fatal("IPv4 ns servers defined but not listening") # setup method specific stuff on listeners 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) # start the firewall fw.setup(subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, required.udp, user, tmark) # start the client process try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dns_listener, seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver) finally: try: if daemon: # it's not our child anymore; can't waitpid fw.p.returncode = 0 fw.done() sdnotify.send(sdnotify.stop()) finally: if daemon: daemon_cleanup() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/cmdline.py0000600000175000017500000001123300000000000016171 0ustar00brianbrianimport re import socket import platform import sshuttle.helpers as helpers import sshuttle.client as client import sshuttle.firewall as firewall import sshuttle.hostwatch as hostwatch import sshuttle.ssyslog as ssyslog from sshuttle.options import parser, parse_ipport from sshuttle.helpers import family_ip_tuple, log, Fatal from sshuttle.sudoers import sudoers def main(): opt = parser.parse_args() if opt.sudoers or opt.sudoers_no_modify: if platform.platform().startswith('OpenBSD'): log('Automatic sudoers does not work on BSD') exit(1) if not opt.sudoers_filename: log('--sudoers-file must be set or omited.') exit(1) sudoers( user_name=opt.sudoers_user, no_modify=opt.sudoers_no_modify, file_name=opt.sudoers_filename ) if opt.daemon: opt.syslog = 1 if opt.wrap: import sshuttle.ssnet as ssnet ssnet.MAX_CHANNEL = opt.wrap if opt.latency_buffer_size: import sshuttle.ssnet as ssnet ssnet.LATENCY_BUFFER_SIZE = opt.latency_buffer_size helpers.verbose = opt.verbose try: if opt.firewall: if opt.subnets or opt.subnets_file: parser.error('exactly zero arguments expected') return firewall.main(opt.method, opt.syslog) elif opt.hostwatch: return hostwatch.hw_main(opt.subnets, opt.auto_hosts) else: # parse_subnetports() is used to create a list of includes # and excludes. It is called once for each parameter and # returns a list of one or more items for each subnet (it # can return more than one item when a hostname in the # parameter resolves to multiple IP addresses. Here, we # flatten these lists. includes = [item for sublist in opt.subnets+opt.subnets_file for item in sublist] excludes = [item for sublist in opt.exclude for item in sublist] if not includes and not opt.auto_nets: parser.error('at least one subnet, subnet file, ' 'or -N expected') remotename = opt.remote if remotename == '' or remotename == '-': remotename = None nslist = [family_ip_tuple(ns) for ns in opt.ns_hosts] if opt.seed_hosts: sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip()) elif opt.auto_hosts: sh = [] else: sh = None if opt.listen: ipport_v6 = None ipport_v4 = None lst = opt.listen.split(",") for ip in lst: family, ip, port = parse_ipport(ip) if family == socket.AF_INET6: ipport_v6 = (ip, port) else: ipport_v4 = (ip, port) else: # parse_ipport4('127.0.0.1:0') ipport_v4 = "auto" # parse_ipport6('[::1]:0') ipport_v6 = "auto" if not opt.disable_ipv6 else None if opt.syslog: ssyslog.start_syslog() ssyslog.close_stdin() ssyslog.stdout_to_syslog() ssyslog.stderr_to_syslog() return_code = client.main(ipport_v6, ipport_v4, opt.ssh_cmd, remotename, opt.python, opt.latency_control, opt.dns, nslist, opt.method, sh, opt.auto_hosts, opt.auto_nets, includes, excludes, opt.daemon, opt.to_ns, opt.pidfile, opt.user, opt.sudo_pythonpath, opt.tmark) if return_code == 0: log('Normal exit code, exiting...') else: log('Abnormal exit code %d detected, failing...' % return_code) return return_code except Fatal as e: log('fatal: %s\n' % e) return 99 except KeyboardInterrupt: log('\n') log('Keyboard interrupt: exiting.\n') return 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/firewall.py0000600000175000017500000002355600000000000016376 0ustar00brianbrianimport errno import socket import signal import sys import os import platform import traceback import sshuttle.ssyslog as ssyslog import sshuttle.helpers as helpers from sshuttle.helpers import debug1, debug2, Fatal from sshuttle.methods import get_auto_method, get_method HOSTSFILE = '/etc/hosts' def rewrite_etc_hosts(hostmap, 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 is not None: os.chown(tmpname, st.st_uid, st.st_gid) os.chmod(tmpname, st.st_mode) else: os.chown(tmpname, 0, 0) os.chmod(tmpname, 0o600) os.rename(tmpname, HOSTSFILE) def restore_etc_hosts(hostmap, port): # Only restore if we added hosts to /etc/hosts previously. if len(hostmap) > 0: debug2('undoing /etc/hosts changes.\n') rewrite_etc_hosts({}, port) # Isolate function that needs to be replaced for tests def setup_daemon(): if os.getuid() != 0: raise Fatal('fw: ' '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 # Note that we're sorting in a very particular order: # we need to go from smaller, more specific, port ranges, to larger, # less-specific, port ranges. At each level, we order by subnet # width, from most-specific subnets (largest swidth) to # least-specific. On ties, excludes come first. # s:(inet, subnet width, exclude flag, subnet, first port, last port) def subnet_weight(s): return (-s[-1] + (s[-2] or -65535), s[1], s[2]) # This is some voodoo for setting up the kernel's transparent # 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() hostmap = {} helpers.logprefix = 'fw: ' debug1('Starting firewall with Python version %s\n' % platform.python_version()) if method_name == "auto": method = get_auto_method() else: method = get_method(method_name) if syslog: ssyslog.start_syslog() ssyslog.stderr_to_syslog() if not method.is_supported(): raise Fatal("The %s method is not supported on this machine. " "Check that the appropriate programs are in your " "PATH." % method_name) debug1('ready method name %s.\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('expected ROUTES but got %r' % line) while 1: line = stdin.readline(128) if not line: raise Fatal('fw: expected route but got %r' % line) elif line.startswith("NSLIST\n"): break try: (family, width, exclude, ip, fport, lport) = \ line.strip().split(',', 5) except BaseException: raise Fatal('fw: expected route or NSLIST but got %r' % line) subnets.append(( int(family), int(width), bool(int(exclude)), ip, int(fport), int(lport))) debug2('Got subnets: %r\n' % subnets) nslist = [] if line != 'NSLIST\n': raise Fatal('fw: expected NSLIST but got %r' % line) while 1: line = stdin.readline(128) if not line: raise Fatal('fw: expected nslist but got %r' % line) elif line.startswith("PORTS "): break try: (family, ip) = line.strip().split(',', 1) except BaseException: raise Fatal('fw: 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('fw: expected PORTS but got %r' % line) _, _, ports = line.partition(" ") ports = ports.split(",") if len(ports) != 4: raise Fatal('fw: expected 4 ports but got %d' % 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('fw: expected GO but got %r' % line) elif not line.startswith("GO "): raise Fatal('fw: expected GO but got %r' % line) _, _, args = line.partition(" ") udp, user = args.strip().split(" ", 1) udp = bool(int(udp)) if user == '-': user = None debug2('Got udp: %r, user: %r\n' % (udp, user)) subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] try: debug1('setting up.\n') if subnets_v6 or nslist_v6: debug2('setting up IPv6.\n') method.setup_firewall( port_v6, dnsport_v6, nslist_v6, socket.AF_INET6, subnets_v6, udp, user) if subnets_v4 or nslist_v4: debug2('setting up IPv4.\n') method.setup_firewall( port_v4, dnsport_v4, nslist_v4, socket.AF_INET, subnets_v4, udp, user) 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: line = stdin.readline(128) if line.startswith('HOST '): (name, ip) = line[5:].strip().split(',', 1) hostmap[name] = ip debug2('setting up /etc/hosts.\n') rewrite_etc_hosts(hostmap, port_v6 or port_v4) elif line: if not method.firewall_command(line): raise Fatal('fw: expected command, got %r' % line) else: break finally: try: debug1('undoing changes.\n') except BaseException: debug2('An error occurred, ignoring it.') try: if subnets_v6 or nslist_v6: debug2('undoing IPv6 changes.\n') method.restore_firewall(port_v6, socket.AF_INET6, udp, user) except BaseException: try: debug1("Error trying to undo IPv6 firewall.\n") for line in traceback.format_exc().splitlines(): debug1("---> %s\n" % line) except BaseException: debug2('An error occurred, ignoring it.') try: if subnets_v4 or nslist_v4: debug2('undoing IPv4 changes.\n') method.restore_firewall(port_v4, socket.AF_INET, udp, user) except BaseException: try: debug1("Error trying to undo IPv4 firewall.\n") for line in traceback.format_exc().splitlines(): debug1("---> %s\n" % line) except BaseException: debug2('An error occurred, ignoring it.') try: # debug2() message printed in restore_etc_hosts() function. restore_etc_hosts(hostmap, port_v6 or port_v4) except BaseException: try: debug1("Error trying to undo /etc/hosts changes.\n") for line in traceback.format_exc().splitlines(): debug1("---> %s\n" % line) except BaseException: debug2('An error occurred, ignoring it.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/sshuttle/helpers.py0000600000175000017500000001476700000000000016237 0ustar00brianbrianimport sys import socket import errno import os logprefix = '' verbose = 0 def b(s): return s.encode("ASCII") def log(s): global logprefix try: sys.stdout.flush() if s.find("\n") != -1: prefix = logprefix s = s.rstrip("\n") for line in s.split("\n"): sys.stderr.write(prefix + line + "\n") prefix = "---> " else: sys.stderr.write(logprefix + s) 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(systemd_resolved): """Retrieves a list of tuples (address type, address as a string) of the DNS servers used by the system to resolve hostnames. If parameter is False, DNS servers are retrieved from only /etc/resolv.conf. This behavior makes sense for the sshuttle server. If parameter is True, we retrieve information from both /etc/resolv.conf and /run/systemd/resolve/resolv.conf (if it exists). This behavior makes sense for the sshuttle client. """ # Historically, we just needed to read /etc/resolv.conf. # # If systemd-resolved is active, /etc/resolv.conf will point to # localhost and the actual DNS servers that systemd-resolved uses # are stored in /run/systemd/resolve/resolv.conf. For programs # that use the localhost DNS server, having sshuttle read # /etc/resolv.conf is sufficient. However, resolved provides other # ways of resolving hostnames (such as via dbus) that may not # route requests through localhost. So, we retrieve a list of DNS # servers that resolved uses so we can intercept those as well. # # For more information about systemd-resolved, see: # https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html # # On machines without systemd-resolved, we expect opening the # second file will fail. files = ['/etc/resolv.conf'] if systemd_resolved: files += ['/run/systemd/resolve/resolv.conf'] nsservers = [] for f in files: this_file_nsservers = [] try: for line in open(f): words = line.lower().split() if len(words) >= 2 and words[0] == 'nameserver': this_file_nsservers.append(family_ip_tuple(words[1])) debug2("Found DNS servers in %s: %s\n" % (f, [n[1] for n in this_file_nsservers])) nsservers += this_file_nsservers except OSError as e: debug3("Failed to read %s when looking for DNS servers: %s\n" % (f, e.strerror)) return nsservers def resolvconf_random_nameserver(systemd_resolved): """Return a random nameserver selected from servers produced by resolvconf_nameservers(). See documentation for resolvconf_nameservers() for a description of the parameter. """ lines = resolvconf_nameservers(systemd_resolved) if lines: if len(lines) > 1: # don't import this unless we really need it import random random.shuffle(lines) return lines[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: _, e = sys.exc_info()[:2] 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) def get_env(): """An environment for sshuttle subprocesses. See get_path().""" env = { 'PATH': get_path(), 'LC_ALL': "C", } return env def get_path(): """Returns a string of paths separated by os.pathsep. Users might not have all of the programs sshuttle needs in their PATH variable (i.e., some programs might be in /sbin). Use PATH and a hardcoded set of paths to search through. This function is used by our which() and get_env() functions. If which() and the subprocess environments differ, programs that which() finds might not be found at run time (or vice versa). """ path = [] if "PATH" in os.environ: path += os.environ["PATH"].split(os.pathsep) # Python default paths. path += os.defpath.split(os.pathsep) # /sbin, etc are not in os.defpath and may not be in PATH either. # /bin/ and /usr/bin below are probably redundant. path += ['/bin', '/usr/bin', '/sbin', '/usr/sbin'] # Remove duplicates. Not strictly necessary. path_dedup = [] for i in path: if i not in path_dedup: path_dedup.append(i) return os.pathsep.join(path_dedup) if sys.version_info >= (3, 3): from shutil import which as _which else: # Although sshuttle does not officially support older versions of # Python, some still run the sshuttle server on remote machines # with old versions of python. def _which(file, mode=os.F_OK | os.X_OK, path=None): if path is not None: search_paths = path.split(os.pathsep) elif "PATH" in os.environ: search_paths = os.environ["PATH"].split(os.pathsep) else: search_paths = os.defpath.split(os.pathsep) for p in search_paths: filepath = os.path.join(p, file) if os.path.exists(filepath) and os.access(filepath, mode): return filepath return None def which(file, mode=os.F_OK | os.X_OK): """A wrapper around shutil.which() that searches a predictable set of paths and is more verbose about what is happening. See get_path() for more information. """ path = get_path() rv = _which(file, mode, path) if rv: debug2("which() found '%s' at %s\n" % (file, rv)) else: debug2("which() could not find '%s' in %s\n" % (file, path)) return rv ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/sshuttle/hostwatch.py0000600000175000017500000001737300000000000016575 0ustar00brianbrianimport time import socket import re import select import errno import os import sys import platform import subprocess as ssubprocess import sshuttle.helpers as helpers from sshuttle.helpers import log, debug1, debug2, debug3, get_env POLL_TIME = 60 * 15 NETSTAT_POLL_TIME = 30 CACHEFILE = os.path.expanduser('~/.sshuttle.hosts') _nmb_ok = True _smb_ok = True hostnames = {} queue = {} try: null = open(os.devnull, 'wb') except IOError: _, e = sys.exc_info()[:2] log('warning: %s\n' % e) null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096) def _is_ip(s): return re.match(r'\d+\.\d+\.\d+\.\d+$', s) def write_host_cache(): tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid()) try: f = open(tmpname, 'wb') for name, ip in sorted(hostnames.items()): f.write(('%s,%s\n' % (name, ip)).encode("ASCII")) f.close() os.chmod(tmpname, 384) # 600 in octal, 'rw-------' os.rename(tmpname, CACHEFILE) finally: try: os.unlink(tmpname) except BaseException: pass def read_host_cache(): try: f = open(CACHEFILE) except IOError: _, e = sys.exc_info()[:2] if e.errno == errno.ENOENT: return else: raise for line in f: words = line.strip().split(',') if len(words) == 2: (name, ip) = words name = re.sub(r'[^-\w\.]', '-', name).strip() ip = re.sub(r'[^0-9.]', '', ip).strip() if name and ip: found_host(name, ip) def found_host(name, ip): hostname = re.sub(r'\..*', '', name) hostname = re.sub(r'[^-\w\.]', '_', hostname) if (ip.startswith('127.') or ip.startswith('255.') or hostname == 'localhost'): return if hostname != name: found_host(hostname, ip) oldip = hostnames.get(name) if oldip != ip: hostnames[name] = ip debug1('Found: %s: %s\n' % (name, ip)) sys.stdout.write('%s,%s\n' % (name, ip)) write_host_cache() def _check_etc_hosts(): debug2(' > hosts\n') for line in open('/etc/hosts'): line = re.sub(r'#.*', '', line) words = line.strip().split() if not words: continue ip = words[0] names = words[1:] if _is_ip(ip): debug3('< %s %r\n' % (ip, names)) for n in names: check_host(n) found_host(n, ip) def _check_revdns(ip): debug2(' > rev: %s\n' % ip) try: r = socket.gethostbyaddr(ip) debug3('< %s\n' % r[0]) check_host(r[0]) found_host(r[0], ip) except (socket.herror, UnicodeError): pass def _check_dns(hostname): debug2(' > dns: %s\n' % hostname) try: ip = socket.gethostbyname(hostname) debug3('< %s\n' % ip) check_host(ip) found_host(hostname, ip) except (socket.gaierror, UnicodeError): pass def _check_netstat(): debug2(' > netstat\n') argv = ['netstat', '-n'] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, env=get_env()) content = p.stdout.read().decode("ASCII") p.wait() except OSError: _, e = sys.exc_info()[:2] log('%r failed: %r\n' % (argv, e)) return for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content): debug3('< %s\n' % ip) check_host(ip) def _check_smb(hostname): return global _smb_ok if not _smb_ok: return debug2(' > smb: %s\n' % hostname) argv = ['smbclient', '-U', '%', '-L', hostname] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, env=get_env()) lines = p.stdout.readlines() p.wait() except OSError: _, e = sys.exc_info()[:2] log('%r failed: %r\n' % (argv, e)) _smb_ok = False return lines.reverse() # junk at top while lines: line = lines.pop().strip() if re.match(r'Server\s+', line): break # server list section: # Server Comment # ------ ------- while lines: line = lines.pop().strip() if not line or re.match(r'-+\s+-+', line): continue if re.match(r'Workgroup\s+Master', line): break words = line.split() hostname = words[0].lower() debug3('< %s\n' % hostname) check_host(hostname) # workgroup list section: # Workgroup Master # --------- ------ while lines: line = lines.pop().strip() if re.match(r'-+\s+', line): continue if not line: break words = line.split() (workgroup, hostname) = (words[0].lower(), words[1].lower()) debug3('< group(%s) -> %s\n' % (workgroup, hostname)) check_host(hostname) check_workgroup(workgroup) if lines: assert(0) def _check_nmb(hostname, is_workgroup, is_master): return global _nmb_ok if not _nmb_ok: return debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname)) argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, env=get_env) lines = p.stdout.readlines() rv = p.wait() except OSError: _, e = sys.exc_info()[:2] log('%r failed: %r\n' % (argv, e)) _nmb_ok = False return if rv: log('%r returned %d\n' % (argv, rv)) return for line in lines: m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line) if m: g = m.groups() (ip, name) = (g[0], g[1].lower()) debug3('< %s -> %s\n' % (name, ip)) if is_workgroup: _enqueue(_check_smb, ip) else: found_host(name, ip) check_host(name) def check_host(hostname): if _is_ip(hostname): _enqueue(_check_revdns, hostname) else: _enqueue(_check_dns, hostname) _enqueue(_check_smb, hostname) _enqueue(_check_nmb, hostname, False, False) def check_workgroup(hostname): _enqueue(_check_nmb, hostname, True, False) _enqueue(_check_nmb, hostname, True, True) def _enqueue(op, *args): t = (op, args) if queue.get(t) is None: queue[t] = 0 def _stdin_still_ok(timeout): r, _, _ = select.select([sys.stdin.fileno()], [], [], timeout) if r: b = os.read(sys.stdin.fileno(), 4096) if not b: return False return True def hw_main(seed_hosts, auto_hosts): if helpers.verbose >= 2: helpers.logprefix = 'HH: ' else: helpers.logprefix = 'hostwatch: ' debug1('Starting hostwatch with Python version %s\n' % platform.python_version()) for h in seed_hosts: check_host(h) if auto_hosts: read_host_cache() _enqueue(_check_etc_hosts) _enqueue(_check_netstat) check_host('localhost') check_host(socket.gethostname()) check_workgroup('workgroup') check_workgroup('-') while 1: now = time.time() for t, last_polled in list(queue.items()): (op, args) = t if not _stdin_still_ok(0): break maxtime = POLL_TIME if op == _check_netstat: maxtime = NETSTAT_POLL_TIME if now - last_polled > maxtime: queue[t] = time.time() op(*args) try: sys.stdout.flush() except IOError: break # FIXME: use a smarter timeout based on oldest last_polled if not _stdin_still_ok(1): break ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/sshuttle/linux.py0000600000175000017500000000445500000000000015725 0ustar00brianbrianimport socket import subprocess as ssubprocess from sshuttle.helpers import log, debug1, Fatal, family_to_string, get_env def nonfatal(func, *args): try: func(*args) except Fatal as e: log('fw: 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'] try: output = ssubprocess.check_output(argv, env=get_env()) for line in output.decode('ASCII').split('\n'): if line.startswith('Chain %s ' % name): return True except ssubprocess.CalledProcessError as e: raise Fatal('fw: %r returned %d' % (argv, e.returncode)) 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, env=get_env()) if rv: raise Fatal('fw: %r returned %d' % (argv, rv)) def nft(family, table, action, *args): if family in (socket.AF_INET, socket.AF_INET6): argv = ['nft', action, 'inet', table] + list(args) else: raise Exception('Unsupported family "%s"' % family_to_string(family)) debug1('%s\n' % ' '.join(argv)) rv = ssubprocess.call(argv, env=get_env()) if rv: raise Fatal('fw: %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 63. This makes the client side not recapture those # connections, in case client == server. try: argsplus = list(args) ipt(family, *argsplus) except Fatal: ipt(family, *args) # we only get here if the non-ttl attempt succeeds log('fw: WARNING: your iptables is missing ' 'the ttl module.\n') _no_ttl_module = True else: ipt(family, *args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/sshuttle/methods/0000700000175000017500000000000000000000000015645 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/methods/__init__.py0000600000175000017500000000703100000000000017761 0ustar00brianbrianimport importlib import socket import struct import errno import ipaddress from sshuttle.helpers import Fatal, debug3 def original_dst(sock): ip = "0.0.0.0" port = -1 try: family = sock.family SO_ORIGINAL_DST = 80 if family == socket.AF_INET: SOCKADDR_MIN = 16 sockaddr_in = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, SOCKADDR_MIN) port, raw_ip = struct.unpack_from('!2xH4s', sockaddr_in[:8]) ip = str(ipaddress.IPv4Address(raw_ip)) elif family == socket.AF_INET6: sockaddr_in = sock.getsockopt(41, SO_ORIGINAL_DST, 64) port, raw_ip = struct.unpack_from("!2xH4x16s", sockaddr_in) ip = str(ipaddress.IPv6Address(raw_ip)) else: raise Fatal("fw: Unknown family type.") except socket.error as e: if e.args[0] == errno.ENOPROTOOPT: return sock.getsockname() raise return (ip, port) class Features(object): pass class BaseMethod(object): def __init__(self, name): self.firewall = None self.name = name def set_firewall(self, firewall): self.firewall = firewall @staticmethod def get_supported_features(): result = Features() result.ipv4 = True result.ipv6 = False result.udp = False result.dns = True result.user = False return result @staticmethod def is_supported(): """Returns true if it appears that this method will work on this machine.""" return False @staticmethod def get_tcp_dstip(sock): return original_dst(sock) @staticmethod def recv_udp(udp_listener, bufsize): debug3('Accept UDP using recvfrom.\n') 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 assert_features(self, features): avail = self.get_supported_features() for key in ["udp", "dns", "ipv6", "ipv4", "user"]: if getattr(features, key) and not getattr(avail, key): raise Fatal( "Feature %s not supported with method %s.\n" % (key, self.name)) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user): raise NotImplementedError() def restore_firewall(self, port, family, udp, user): raise NotImplementedError() @staticmethod def firewall_command(line): return False def get_method(method_name): module = importlib.import_module("sshuttle.methods.%s" % method_name) return module.Method(method_name) def get_auto_method(): debug3("Selecting a method automatically...\n") # Try these methods, in order: methods_to_try = ["nat", "nft", "pf", "ipfw"] for m in methods_to_try: method = get_method(m) if method.is_supported(): debug3("Method '%s' was automatically selected.\n" % m) return method raise Fatal("Unable to automatically find a supported method. Check that " "the appropriate programs are in your PATH. We tried " "methods: %s" % str(methods_to_try)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/methods/ipfw.py0000600000175000017500000002105500000000000017171 0ustar00brianbrianimport os import subprocess as ssubprocess from sshuttle.methods import BaseMethod from sshuttle.helpers import log, debug1, debug2, debug3, \ Fatal, family_to_string, get_env, which recvmsg = None try: # try getting recvmsg from python import socket as pythonsocket getattr(pythonsocket.socket, "recvmsg") socket = pythonsocket recvmsg = "python" except AttributeError: # try getting recvmsg from socket_ext library try: import socket_ext getattr(socket_ext.socket, "recvmsg") socket = socket_ext recvmsg = "socket_ext" except ImportError: import socket IP_BINDANY = 24 IP_RECVDSTADDR = 7 SOL_IPV6 = 41 IPV6_RECVDSTADDR = 74 if recvmsg == "python": def recv_udp(listener, bufsize): debug3('Accept UDP python using recvmsg.\n') data, ancdata, _, srcip = listener.recvmsg(4096, socket.CMSG_SPACE(4)) dstip = None for cmsg_level, cmsg_type, cmsg_data in ancdata: if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR: port = 53 ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4]) dstip = (ip, port) break return (srcip, dstip, data) elif recvmsg == "socket_ext": def recv_udp(listener, bufsize): debug3('Accept UDP using socket_ext recvmsg.\n') srcip, data, adata, _ = listener.recvmsg((bufsize,), socket.CMSG_SPACE(4)) dstip = None for a in adata: if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR: port = 53 ip = socket.inet_ntop(socket.AF_INET, a.cmsg_data[0:4]) dstip = (ip, port) break return (srcip, dstip, data[0]) else: def recv_udp(listener, bufsize): debug3('Accept UDP using recvfrom.\n') data, srcip = listener.recvfrom(bufsize) return (srcip, None, data) def ipfw_rule_exists(n): argv = ['ipfw', 'list'] p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env()) found = False for line in p.stdout: if line.startswith(b'%05d ' % n): if not ('ipttl 63' 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, env=get_env()) for line in p.stdout: line = line.decode() assert(line[-1] == '\n') (k, v) = line[:-1].split(': ', 1) _oldctls[k] = v.strip() rv = p.wait() if rv: raise Fatal('%r returned %d' % (argv, rv)) if not line: raise Fatal('%r returned no data' % (argv,)) def _sysctl_set(name, val): argv = ['sysctl', '-w', '%s=%s' % (name, val)] debug1('>> %s\n' % ' '.join(argv)) return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env()) # No env: No output. (Or error that won't be parsed.) _changedctls = [] def sysctl_set(name, val, permanent=False): PREFIX = 'net.inet.ip' assert(name.startswith(PREFIX + '.')) val = str(val) if not _oldctls: _fill_oldctls(PREFIX) if not (name in _oldctls): debug1('>> No such sysctl: %r\n' % name) return False oldval = _oldctls[name] if val != oldval: rv = _sysctl_set(name, val) if rv == 0 and permanent: debug1('>> ...saving permanently in /etc/sysctl.conf\n') f = open('/etc/sysctl.conf', 'a') f.write('\n' '# Added by sshuttle\n' '%s=%s\n' % (name, val)) f.close() else: _changedctls.append(name) return True def ipfw(*args): argv = ['ipfw', '-q'] + list(args) debug1('>> %s\n' % ' '.join(argv)) rv = ssubprocess.call(argv, env=get_env()) # No env: No output. (Or error that won't be parsed.) if rv: raise Fatal('%r returned %d' % (argv, rv)) def ipfw_noexit(*args): argv = ['ipfw', '-q'] + list(args) debug1('>> %s\n' % ' '.join(argv)) ssubprocess.call(argv, env=get_env()) # No env: No output. (Or error that won't be parsed.) class Method(BaseMethod): def get_supported_features(self): result = super(Method, self).get_supported_features() result.ipv6 = False result.udp = False # NOTE: Almost there, kernel patch needed result.dns = True return result def get_tcp_dstip(self, sock): return sock.getsockname() def recv_udp(self, udp_listener, bufsize): srcip, dstip, data = recv_udp(udp_listener, bufsize) if not dstip: debug1( "-- ignored UDP from %r: " "couldn't determine destination IP address\n" % (srcip,)) return None return srcip, dstip, data def send_udp(self, sock, srcip, dstip, data): if not srcip: debug1( "-- ignored UDP to %r: " "couldn't determine source IP address\n" % (dstip,)) return # debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip)) sender = socket.socket(sock.family, socket.SOCK_DGRAM) sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1) sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 63) sender.bind(srcip) sender.sendto(data, dstip) sender.close() def setup_udp_listener(self, udp_listener): if udp_listener.v4 is not None: udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1) # if udp_listener.v6 is not None: # udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user): # IPv6 not supported if family not in [socket.AF_INET]: raise Exception( 'Address family "%s" unsupported by ipfw method_name' % family_to_string(family)) # XXX: Any risk from this? ipfw_noexit('delete', '1') while _changedctls: name = _changedctls.pop() oldval = _oldctls[name] _sysctl_set(name, oldval) if subnets or dnsport: sysctl_set('net.inet.ip.fw.enable', 1) ipfw('add', '1', 'check-state', 'ip', 'from', 'any', 'to', 'any') ipfw('add', '1', 'skipto', '2', 'tcp', 'from', 'any', 'to', 'table(125)') ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port, 'tcp', 'from', 'any', 'to', 'table(126)', 'not', 'ipttl', '63', 'keep-state', 'setup') ipfw_noexit('table', '124', 'flush') dnscount = 0 for _, ip in [i for i in nslist if i[0] == family]: ipfw('table', '124', 'add', '%s' % (ip)) dnscount += 1 if dnscount > 0: ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport, 'udp', 'from', 'any', 'to', 'table(124)', 'not', 'ipttl', '63') ipfw('add', '1', 'allow', 'udp', 'from', 'any', 'to', 'any', 'ipttl', '63') if subnets: # create new subnet entries for _, swidth, sexclude, snet in sorted(subnets, key=lambda s: s[1], reverse=True): if sexclude: ipfw('table', '125', 'add', '%s/%s' % (snet, swidth)) else: ipfw('table', '126', 'add', '%s/%s' % (snet, swidth)) def restore_firewall(self, port, family, udp, user): if family not in [socket.AF_INET]: raise Exception( 'Address family "%s" unsupported by tproxy method' % family_to_string(family)) ipfw_noexit('delete', '1') ipfw_noexit('table', '124', 'flush') ipfw_noexit('table', '125', 'flush') ipfw_noexit('table', '126', 'flush') def is_supported(self): if which("ipfw"): return True debug2("ipfw method not supported because 'ipfw' command is " "missing.\n") return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/methods/nat.py0000600000175000017500000001133000000000000017001 0ustar00brianbrianimport socket from sshuttle.firewall import subnet_weight from sshuttle.helpers import family_to_string, which, debug2 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, user): # 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) def _ipm(*args): return ipt(family, "mangle", *args) chain = 'sshuttle-%s' % port # basic cleanup/setup of chains self.restore_firewall(port, family, udp, user) _ipt('-N', chain) _ipt('-F', chain) if user is not None: _ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user), '-j', 'MARK', '--set-mark', str(port)) args = '-m', 'mark', '--mark', str(port), '-j', chain else: args = '-j', chain _ipt('-I', 'OUTPUT', '1', *args) _ipt('-I', 'PREROUTING', '1', *args) # This TTL hack allows the client and server to run on the # same host. The connections the sshuttle server makes will # have TTL set to 63. _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', '63') # Redirect DNS traffic as requested. This includes routing traffic # to localhost DNS servers through sshuttle. for _, ip in [i for i in nslist if i[0] == family]: _ipt('-A', chain, '-j', 'REDIRECT', '--dest', '%s/32' % ip, '-p', 'udp', '--dport', '53', '--to-ports', str(dnsport)) # Don't route any remaining local traffic through sshuttle. _ipt('-A', chain, '-j', 'RETURN', '-m', 'addrtype', '--dst-type', 'LOCAL') # create new subnet entries. for _, swidth, sexclude, snet, fport, lport \ in sorted(subnets, key=subnet_weight, reverse=True): tcp_ports = ('-p', 'tcp') if fport: tcp_ports = tcp_ports + ('--dport', '%d:%d' % (fport, lport)) if sexclude: _ipt('-A', chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet, swidth), *tcp_ports) else: _ipt('-A', chain, '-j', 'REDIRECT', '--dest', '%s/%s' % (snet, swidth), *(tcp_ports + ('--to-ports', str(port)))) def restore_firewall(self, port, family, udp, user): # 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) def _ipm(*args): return ipt(family, "mangle", *args) chain = 'sshuttle-%s' % port # basic cleanup/setup of chains if ipt_chain_exists(family, table, chain): if user is not None: nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner', str(user), '-j', 'MARK', '--set-mark', str(port)) args = '-m', 'mark', '--mark', str(port), '-j', chain else: args = '-j', chain nonfatal(_ipt, '-D', 'OUTPUT', *args) nonfatal(_ipt, '-D', 'PREROUTING', *args) nonfatal(_ipt, '-F', chain) _ipt('-X', chain) def get_supported_features(self): result = super(Method, self).get_supported_features() result.user = True return result def is_supported(self): if which("iptables"): return True debug2("nat method not supported because 'iptables' command " "is missing.\n") return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/methods/nft.py0000600000175000017500000001135100000000000017011 0ustar00brianbrianimport socket from sshuttle.firewall import subnet_weight from sshuttle.linux import nft, nonfatal from sshuttle.methods import BaseMethod from sshuttle.helpers import debug2, which class Method(BaseMethod): # We name the chain based on the transproxy port number so that it's # possible to run multiple copies of sshuttle at the same time. Of course, # the multiple copies shouldn't have overlapping subnets, or only the most- # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user): if udp: raise Exception("UDP not supported by nft") if family == socket.AF_INET: table = 'sshuttle-ipv4-%s' % port if family == socket.AF_INET6: table = 'sshuttle-ipv6-%s' % port def _nft(action, *args): return nft(family, table, action, *args) chain = table # basic cleanup/setup of chains _nft('add table', '') _nft('add chain', 'prerouting', '{ type nat hook prerouting priority -100; policy accept; }') _nft('add chain', 'output', '{ type nat hook output priority -100; policy accept; }') _nft('add chain', chain) _nft('flush chain', chain) _nft('add rule', 'output jump %s' % chain) _nft('add rule', 'prerouting jump %s' % chain) # setup_firewall() gets called separately for ipv4 and ipv6. Make sure # we only handle the version that we expect to. if family == socket.AF_INET: _nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv4', 'return') else: _nft('add rule', chain, 'meta', 'nfproto', '!=', 'ipv6', 'return') # This TTL hack allows the client and server to run on the # same host. The connections the sshuttle server makes will # have TTL set to 63. if family == socket.AF_INET: _nft('add rule', chain, 'ip ttl == 63 return') elif family == socket.AF_INET6: _nft('add rule', chain, 'ip6 hoplimit == 63 return') # Strings to use below to simplify our code if family == socket.AF_INET: ip_version_l = 'ipv4' ip_version = 'ip' elif family == socket.AF_INET6: ip_version_l = 'ipv6' ip_version = 'ip6' # Redirect DNS traffic as requested. This includes routing traffic # to localhost DNS servers through sshuttle. for _, ip in [i for i in nslist if i[0] == family]: _nft('add rule', chain, ip_version, 'daddr %s' % ip, 'udp dport 53', ('redirect to :' + str(dnsport))) # Don't route any remaining local traffic through sshuttle _nft('add rule', chain, 'fib daddr type local return') # create new subnet entries. for _, swidth, sexclude, snet, fport, lport \ in sorted(subnets, key=subnet_weight, reverse=True): # match using nfproto as described at # https://superuser.com/questions/1560376/match-ipv6-protocol-using-nftables if fport and fport != lport: tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp', 'dport', '{ %d-%d }' % (fport, lport)) elif fport and fport == lport: tcp_ports = ('meta', 'nfproto', ip_version_l, 'tcp', 'dport', '%d' % (fport)) else: tcp_ports = ('meta', 'nfproto', ip_version_l, 'meta', 'l4proto', 'tcp') if sexclude: _nft('add rule', chain, *(tcp_ports + ( ip_version, 'daddr %s/%s' % (snet, swidth), 'return'))) else: _nft('add rule', chain, *(tcp_ports + ( ip_version, 'daddr %s/%s' % (snet, swidth), ('redirect to :' + str(port))))) def restore_firewall(self, port, family, udp, user): if udp: raise Exception("UDP not supported by nft method_name") if family == socket.AF_INET: table = 'sshuttle-ipv4-%s' % port if family == socket.AF_INET6: table = 'sshuttle-ipv6-%s' % port def _nft(action, *args): return nft(family, table, action, *args) # basic cleanup/setup of chains nonfatal(_nft, 'delete table', '') def get_supported_features(self): result = super(Method, self).get_supported_features() result.ipv6 = True return result def is_supported(self): if which("nft"): return True debug2("nft method not supported because 'nft' command is missing.\n") return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/methods/pf.py0000600000175000017500000004173500000000000016640 0ustar00brianbrianimport os import sys import platform import re import socket import errno import struct import subprocess as ssubprocess import shlex from fcntl import ioctl from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \ sizeof, addressof, memmove from sshuttle.firewall import subnet_weight from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string, \ get_env, which from sshuttle.methods import BaseMethod _pf_context = { 'started_by_sshuttle': 0, 'loaded_by_sshuttle': True, 'Xtoken': [] } _pf_fd = None class Generic(object): MAXPATHLEN = 1024 PF_CHANGE_ADD_TAIL = 2 PF_CHANGE_GET_TICKET = 6 PF_PASS = 0 PF_RDR = 8 PF_OUT = 2 ACTION_OFFSET = 0 POOL_TICKET_OFFSET = 8 ANCHOR_CALL_OFFSET = 1040 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",) def __init__(self): self.status = b'' self.pfioc_pooladdr = c_char * 1136 self.DIOCNATLOOK = ( (0x40000000 | 0x80000000) | ((sizeof(self.pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23)) self.DIOCCHANGERULE = ( (0x40000000 | 0x80000000) | ((sizeof(self.pfioc_rule) & 0x1fff) << 16) | ((ord('D')) << 8) | (26)) self.DIOCBEGINADDRS = ( (0x40000000 | 0x80000000) | ((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) | ((ord('D')) << 8) | (51)) def enable(self): if b'INFO:\nStatus: Disabled' in self.status: pfctl('-e') _pf_context['started_by_sshuttle'] += 1 @staticmethod def disable(anchor): pfctl('-a %s -F all' % anchor) if _pf_context['started_by_sshuttle'] == 1: pfctl('-d') _pf_context['started_by_sshuttle'] -= 1 def query_nat(self, family, proto, src_ip, src_port, dst_ip, dst_port): [proto, family, src_port, dst_port] = [ int(v) for v in [proto, family, src_port, dst_port]] packed_src_ip = socket.inet_pton(family, src_ip) packed_dst_ip = socket.inet_pton(family, dst_ip) assert len(packed_src_ip) == len(packed_dst_ip) length = len(packed_src_ip) pnl = self.pfioc_natlook() pnl.proto = proto pnl.direction = self.PF_OUT pnl.af = family memmove(addressof(pnl.saddr), packed_src_ip, length) memmove(addressof(pnl.daddr), packed_dst_ip, length) self._add_natlook_ports(pnl, src_port, dst_port) ioctl(pf_get_dev(), self.DIOCNATLOOK, (c_char * sizeof(pnl)).from_address(addressof(pnl))) ip = socket.inet_ntop( pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw) port = socket.ntohs(self._get_natlook_port(pnl.rdxport)) return (ip, port) @staticmethod def _add_natlook_ports(pnl, src_port, dst_port): pnl.sxport = socket.htons(src_port) pnl.dxport = socket.htons(dst_port) @staticmethod def _get_natlook_port(xport): return xport def add_anchors(self, anchor, status=None): if status is None: status = pfctl('-s all')[0] self.status = status if ('\nanchor "%s"' % anchor).encode('ASCII') not in status: self._add_anchor_rule(self.PF_PASS, anchor.encode('ASCII')) def _add_anchor_rule(self, kind, name, pr=None): if pr is None: pr = self.pfioc_rule() memmove(addressof(pr) + self.ANCHOR_CALL_OFFSET, name, min(self.MAXPATHLEN, len(name))) # anchor_call = name memmove(addressof(pr) + self.RULE_ACTION_OFFSET, struct.pack('I', kind), 4) # rule.action = kind memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack('I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack('I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) @staticmethod def _inet_version(family): return b'inet' if family == socket.AF_INET else b'inet6' @staticmethod def _lo_addr(family): return b'127.0.0.1' if family == socket.AF_INET else b'::1' @staticmethod def add_rules(anchor, rules): assert isinstance(rules, bytes) debug3("rules:\n" + rules.decode("ASCII")) pfctl('-a %s -f /dev/stdin' % anchor, rules) @staticmethod def has_skip_loopback(): return b'skip' in pfctl('-s Interfaces -i lo -v')[0] class FreeBsd(Generic): RULE_ACTION_OFFSET = 2968 def __new__(cls): class pfioc_natlook(Structure): pf_addr = Generic.pf_addr _fields_ = [("saddr", pf_addr), ("daddr", pf_addr), ("rsaddr", pf_addr), ("rdaddr", pf_addr), ("sxport", c_uint16), ("dxport", c_uint16), ("rsxport", c_uint16), ("rdxport", c_uint16), ("af", c_uint8), # sa_family_t ("proto", c_uint8), ("proto_variant", c_uint8), ("direction", c_uint8)] freebsd = Generic.__new__(cls) freebsd.pfioc_rule = c_char * 3040 freebsd.pfioc_natlook = pfioc_natlook return freebsd def enable(self): returncode = ssubprocess.call(['kldload', 'pf'], env=get_env()) # No env: No output. super(FreeBsd, self).enable() if returncode == 0: _pf_context['loaded_by_sshuttle'] = True def disable(self, anchor): super(FreeBsd, self).disable(anchor) if _pf_context['loaded_by_sshuttle'] and \ _pf_context['started_by_sshuttle'] == 0: ssubprocess.call(['kldunload', 'pf'], env=get_env()) # No env: No output. def add_anchors(self, anchor): status = pfctl('-s all')[0] if ('\nrdr-anchor "%s"' % anchor).encode('ASCII') not in status: self._add_anchor_rule(self.PF_RDR, anchor.encode('ASCII')) super(FreeBsd, self).add_anchors(anchor, status=status) def _add_anchor_rule(self, kind, name, pr=None): pr = pr or self.pfioc_rule() ppa = self.pfioc_pooladdr() ioctl(pf_get_dev(), self.DIOCBEGINADDRS, ppa) # pool ticket memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4) super(FreeBsd, self)._add_anchor_rule(kind, name, pr=pr) def add_rules(self, anchor, includes, port, dnsport, nslist, family): inet_version = self._inet_version(family) lo_addr = self._lo_addr(family) tables = [] translating_rules = [ b'rdr pass on lo0 %s proto tcp from ! %s to %s ' b'-> %s port %r' % (inet_version, lo_addr, subnet, lo_addr, port) for exclude, subnet in includes if not exclude ] filtering_rules = [ b'pass out route-to lo0 %s proto tcp ' b'to %s keep state' % (inet_version, subnet) if not exclude else b'pass out %s proto tcp to %s' % (inet_version, subnet) for exclude, subnet in includes ] if nslist: tables.append( b'table {%s}' % b','.join([ns[1].encode("ASCII") for ns in nslist])) translating_rules.append( b'rdr pass on lo0 %s proto udp to ' b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport)) filtering_rules.append( b'pass out route-to lo0 %s proto udp to ' b' port 53 keep state' % inet_version) rules = b'\n'.join(tables + translating_rules + filtering_rules) \ + b'\n' super(FreeBsd, self).add_rules(anchor, rules) class OpenBsd(Generic): POOL_TICKET_OFFSET = 4 RULE_ACTION_OFFSET = 3324 ANCHOR_CALL_OFFSET = 1036 def __init__(self): class pfioc_natlook(Structure): pf_addr = Generic.pf_addr _fields_ = [("saddr", pf_addr), ("daddr", pf_addr), ("rsaddr", pf_addr), ("rdaddr", pf_addr), ("rdomain", c_uint16), ("rrdomain", c_uint16), ("sxport", c_uint16), ("dxport", c_uint16), ("rsxport", c_uint16), ("rdxport", c_uint16), ("af", c_uint8), # sa_family_t ("proto", c_uint8), ("proto_variant", c_uint8), ("direction", c_uint8)] self.pfioc_rule = c_char * 3424 self.pfioc_natlook = pfioc_natlook super(OpenBsd, self).__init__() def add_anchors(self, anchor): # before adding anchors and rules we must override the skip lo # that comes by default in openbsd pf.conf so the rules we will add, # which rely on translating/filtering packets on lo, can work if self.has_skip_loopback(): pfctl('-f /dev/stdin', b'match on lo\n') super(OpenBsd, self).add_anchors(anchor) def add_rules(self, anchor, includes, port, dnsport, nslist, family): inet_version = self._inet_version(family) lo_addr = self._lo_addr(family) tables = [] translating_rules = [ b'pass in on lo0 %s proto tcp to %s ' b'divert-to %s port %r' % (inet_version, subnet, lo_addr, port) for exclude, subnet in includes if not exclude ] filtering_rules = [ b'pass out %s proto tcp to %s ' b'route-to lo0 keep state' % (inet_version, subnet) if not exclude else b'pass out %s proto tcp to %s' % (inet_version, subnet) for exclude, subnet in includes ] if nslist: tables.append( b'table {%s}' % b','.join([ns[1].encode("ASCII") for ns in nslist])) translating_rules.append( b'pass in on lo0 %s proto udp to port 53 ' b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport)) filtering_rules.append( b'pass out %s proto udp to port 53 ' b'route-to lo0 keep state' % inet_version) rules = b'\n'.join(tables + translating_rules + filtering_rules) \ + b'\n' super(OpenBsd, self).add_rules(anchor, rules) class Darwin(FreeBsd): RULE_ACTION_OFFSET = 3068 def __init__(self): class pf_state_xport(Union): _fields_ = [("port", c_uint16), ("call_id", c_uint16), ("spi", c_uint32)] class pfioc_natlook(Structure): pf_addr = Generic.pf_addr _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)] self.pfioc_rule = c_char * 3104 self.pfioc_natlook = pfioc_natlook super(Darwin, self).__init__() def enable(self): o = pfctl('-E') _pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1)) def disable(self, anchor): pfctl('-a %s -F all' % anchor) if _pf_context['Xtoken']: pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII")) def add_anchors(self, anchor): # before adding anchors and rules we must override the skip lo # that in some cases ends up in the chain so the rules we will add, # which rely on translating/filtering packets on lo, can work if self.has_skip_loopback(): pfctl('-f /dev/stdin', b'pass on lo\n') super(Darwin, self).add_anchors(anchor) def _add_natlook_ports(self, pnl, src_port, dst_port): pnl.sxport.port = socket.htons(src_port) pnl.dxport.port = socket.htons(dst_port) def _get_natlook_port(self, xport): return xport.port class PfSense(FreeBsd): RULE_ACTION_OFFSET = 3040 def __init__(self): self.pfioc_rule = c_char * 3112 super(PfSense, self).__init__() if sys.platform == 'darwin': pf = Darwin() elif sys.platform.startswith('openbsd'): pf = OpenBsd() elif platform.version().endswith('pfSense'): pf = PfSense() else: pf = FreeBsd() def pfctl(args, stdin=None): argv = ['pfctl'] + shlex.split(args) debug1('>> %s\n' % ' '.join(argv)) p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE, stdout=ssubprocess.PIPE, stderr=ssubprocess.PIPE, env=get_env()) o = p.communicate(stdin) if p.returncode: raise Fatal('%r returned %d' % (argv, p.returncode)) return o 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_get_anchor(family, port): return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port) class Method(BaseMethod): def get_supported_features(self): result = super(Method, self).get_supported_features() result.ipv6 = True return result def get_tcp_dstip(self, sock): pfile = self.firewall.pfile try: peer = sock.getpeername() except socket.error: _, e = sys.exc_info()[:2] if e.args[0] == errno.EINVAL: return sock.getsockname() proxy = sock.getsockname() argv = (sock.family, socket.IPPROTO_TCP, peer[0].encode("ASCII"), peer[1], proxy[0].encode("ASCII"), proxy[1]) out_line = b"QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv pfile.write(out_line) pfile.flush() in_line = pfile.readline() debug2(out_line.decode("ASCII") + ' > ' + in_line.decode("ASCII")) if in_line.startswith(b'QUERY_PF_NAT_SUCCESS '): (ip, port) = in_line[21:].split(b',') return (ip.decode("ASCII"), int(port)) return sock.getsockname() def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user): if family not in [socket.AF_INET, socket.AF_INET6]: 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 _, swidth, sexclude, snet, fport, lport \ in sorted(subnets, key=subnet_weight): includes.append((sexclude, b"%s/%d%s" % ( snet.encode("ASCII"), swidth, b" port %d:%d" % (fport, lport) if fport else b""))) anchor = pf_get_anchor(family, port) pf.add_anchors(anchor) pf.add_rules(anchor, includes, port, dnsport, nslist, family) pf.enable() def restore_firewall(self, port, family, udp, user): if family not in [socket.AF_INET, socket.AF_INET6]: 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") pf.disable(pf_get_anchor(family, port)) 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 def is_supported(self): if which("pfctl"): return True debug2("pf method not supported because 'pfctl' command is missing.\n") return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/methods/tproxy.py0000600000175000017500000002640000000000000017570 0ustar00brianbrianimport struct from sshuttle.firewall import subnet_weight from sshuttle.helpers import family_to_string from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists from sshuttle.methods import BaseMethod from sshuttle.helpers import debug1, debug2, debug3, Fatal, which recvmsg = None try: # try getting recvmsg from python import socket as pythonsocket getattr(pythonsocket.socket, "recvmsg") socket = pythonsocket recvmsg = "python" except AttributeError: # try getting recvmsg from socket_ext library try: import socket_ext getattr(socket_ext.socket, "recvmsg") socket = socket_ext recvmsg = "socket_ext" except ImportError: import socket 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, _, 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, _ = 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 if recvmsg is None: result.udp = False result.dns = False else: result.udp = True result.dns = True return result def get_tcp_dstip(self, sock): return sock.getsockname() def recv_udp(self, udp_listener, bufsize): srcip, dstip, data = recv_udp(udp_listener, bufsize) if not dstip: debug1( "-- ignored UDP from %r: " "couldn't determine destination IP address\n" % (srcip,)) return None return srcip, dstip, data def send_udp(self, sock, srcip, dstip, data): if not srcip: debug1( "-- ignored UDP to %r: " "couldn't determine source IP address\n" % (dstip,)) return 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, user): if self.firewall is None: tmark = '1' else: tmark = self.firewall.tmark self.setup_firewall_tproxy(port, dnsport, nslist, family, subnets, udp, user, tmark) def setup_firewall_tproxy(self, port, dnsport, nslist, family, subnets, udp, user, tmark): 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) def _ipt_proto_ports(proto, fport, lport): return proto + ('--dport', '%d:%d' % (fport, lport)) \ if fport else proto mark_chain = 'sshuttle-m-%s' % port tproxy_chain = 'sshuttle-t-%s' % port divert_chain = 'sshuttle-d-%s' % port # basic cleanup/setup of chains self.restore_firewall(port, family, udp, user) _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', tmark, '-j', mark_chain) _ipt('-I', 'PREROUTING', tmark, '-j', tproxy_chain) _ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', tmark) _ipt('-A', divert_chain, '-j', 'ACCEPT') _ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'tcp', '-p', 'tcp') if udp: _ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'udp', '-p', 'udp') for _, ip in [i for i in nslist if i[0] == family]: _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark, '--dest', '%s/32' % ip, '-m', 'udp', '-p', 'udp', '--dport', '53') _ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x'+tmark+'/0x'+tmark, '--dest', '%s/32' % ip, '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', str(dnsport)) for _, swidth, sexclude, snet, fport, lport \ in sorted(subnets, key=subnet_weight, reverse=True): tcp_ports = ('-p', 'tcp') tcp_ports = _ipt_proto_ports(tcp_ports, fport, lport) if sexclude: _ipt('-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet, swidth), '-m', 'tcp', *tcp_ports) _ipt('-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet, swidth), '-m', 'tcp', *tcp_ports) else: _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark, '--dest', '%s/%s' % (snet, swidth), '-m', 'tcp', *tcp_ports) _ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x'+tmark+'/0x'+tmark, '--dest', '%s/%s' % (snet, swidth), '-m', 'tcp', *(tcp_ports + ('--on-port', str(port)))) if udp: udp_ports = ('-p', 'udp') udp_ports = _ipt_proto_ports(udp_ports, fport, lport) if sexclude: _ipt('-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet, swidth), '-m', 'udp', *udp_ports) _ipt('-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet, swidth), '-m', 'udp', *udp_ports) else: _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', tmark, '--dest', '%s/%s' % (snet, swidth), '-m', 'udp', *udp_ports) _ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x'+tmark+'/0x'+tmark, '--dest', '%s/%s' % (snet, swidth), '-m', 'udp', *(udp_ports + ('--on-port', str(port)))) def restore_firewall(self, port, family, udp, user): 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) def is_supported(self): if which("iptables") and which("ip6tables"): return True debug2("tproxy method not supported because 'iptables' " "or 'ip6tables' commands are missing.\n") return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/sshuttle/options.py0000600000175000017500000002465200000000000016262 0ustar00brianbrianimport re import socket from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal from sshuttle import __version__ # Subnet file, supporting empty lines and hash-started comment lines def parse_subnetport_file(s): try: handle = open(s, 'r') except OSError: raise Fatal('Unable to open subnet file: %s' % s) raw_config_lines = handle.readlines() subnets = [] for _, line in enumerate(raw_config_lines): line = line.strip() if not line: continue if line[0] == '#': continue subnets.append(parse_subnetport(line)) return subnets # 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4 # [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3 # example.com:123 or just example.com # # In addition, the port number can be specified as a range: # 1.2.3.4:8000-8080. # # Can return multiple matches if the domain name used in the request # has multiple IP addresses. def parse_subnetport(s): if s.count(':') > 1: rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$' else: rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$' m = re.match(rx, s) if not m: raise Fatal('%r is not a valid address/mask:port format' % s) # Ports range from fport to lport. If only one port is specified, # fport is defined and lport is None. # # cidr is the mask defined with the slash notation host, cidr, fport, lport = m.groups() try: addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM) except socket.gaierror: raise Fatal('Unable to resolve address: %s' % host) # If the address is a domain with multiple IPs and a mask is also # provided, proceed cautiously: if cidr is not None: addr_v6 = [a for a in addrinfo if a[0] == socket.AF_INET6] addr_v4 = [a for a in addrinfo if a[0] == socket.AF_INET] # Refuse to proceed if IPv4 and IPv6 addresses are present: if len(addr_v6) > 0 and len(addr_v4) > 0: raise Fatal("%s has IPv4 and IPv6 addresses, so the mask " "of /%s is not supported. Specify the IP " "addresses directly if you wish to specify " "a mask." % (host, cidr)) # Warn if a domain has multiple IPs of the same type (IPv4 vs # IPv6) and the mask is applied to all of the IPs. if len(addr_v4) > 1 or len(addr_v6) > 1: print("WARNING: %s has multiple IP addresses. The " "mask of /%s is applied to all of the addresses." % (host, cidr)) rv = [] for a in addrinfo: family, _, _, _, addr = a # Largest possible slash value we can use with this IP: max_cidr = 32 if family == socket.AF_INET else 128 if cidr is None: # if no mask, use largest mask cidr_to_use = max_cidr else: # verify user-provided mask is appropriate cidr_to_use = int(cidr) if not 0 <= cidr_to_use <= max_cidr: raise Fatal('Slash in CIDR notation (/%d) is ' 'not between 0 and %d' % (cidr_to_use, max_cidr)) rv.append((family, addr[0], cidr_to_use, int(fport or 0), int(lport or fport or 0))) return rv # 1.2.3.4:567 or just 1.2.3.4 or just 567 # [1:2::3]:456 or [1:2::3] or just [::]:567 # example.com:123 or just example.com def parse_ipport(s): s = str(s) if s.isdigit(): rx = r'()(\d+)$' elif ']' in s: rx = r'(?:\[([^]]+)])(?::(\d+))?$' else: rx = r'([\w\.\-]+)(?::(\d+))?$' m = re.match(rx, s) if not m: raise Fatal('%r is not a valid IP:port format' % s) host, port = m.groups() host = host or '0.0.0.0' port = int(port or 0) try: addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) except socket.gaierror: raise Fatal('Unable to resolve address: %s' % host) if len(addrinfo) > 1: print("WARNING: Host %s has more than one IP, only using one of them." % host) family, _, _, _, addr = min(addrinfo) # Note: addr contains (ip, port) return (family,) + addr[:2] def parse_list(lst): return re.split(r'[\s,]+', lst.strip()) if lst else [] class Concat(Action): def __init__(self, option_strings, dest, nargs=None, **kwargs): if nargs is not None: raise ValueError("nargs not supported") super(Concat, self).__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): curr_value = getattr(namespace, self.dest, None) or [] setattr(namespace, self.dest, curr_value + values) parser = ArgumentParser( prog="sshuttle", usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] ", fromfile_prefix_chars="@" ) parser.add_argument( "subnets", metavar="IP/MASK[:PORT[-PORT]]...", nargs="*", type=parse_subnetport, help=""" capture and forward traffic to these subnets (whitespace separated) """ ) parser.add_argument( "-l", "--listen", metavar="[IP:]PORT", help=""" transproxy to this ip address and port number """ ) parser.add_argument( "-H", "--auto-hosts", action="store_true", help=""" continuously scan for remote hostnames and update local /etc/hosts as they are found """ ) parser.add_argument( "-N", "--auto-nets", action="store_true", help=""" automatically determine subnets to route """ ) parser.add_argument( "--dns", action="store_true", help=""" capture local DNS requests and forward to the remote DNS server """ ) parser.add_argument( "--ns-hosts", metavar="IP[,IP]", default=[], type=parse_list, help=""" capture and forward DNS requests made to the following servers """ ) parser.add_argument( "--to-ns", metavar="IP[:PORT]", type=parse_ipport, help=""" the DNS server to forward requests to; defaults to servers in /etc/resolv.conf on remote side if not given. """ ) parser.add_argument( "--method", choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"], metavar="TYPE", default="auto", help=""" %(choices)s """ ) parser.add_argument( "--python", metavar="PATH", help=""" path to python interpreter on the remote server """ ) parser.add_argument( "-r", "--remote", metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]", help=""" ssh hostname (and optional username and password) of remote %(prog)s server """ ) parser.add_argument( "-x", "--exclude", metavar="IP/MASK[:PORT[-PORT]]", action="append", default=[], type=parse_subnetport, help=""" exclude this subnet (can be used more than once) """ ) parser.add_argument( "-X", "--exclude-from", metavar="PATH", action=Concat, dest="exclude", type=parse_subnetport_file, help=""" exclude the subnets in a file (whitespace separated) """ ) parser.add_argument( "-v", "--verbose", action="count", default=0, help=""" increase debug message verbosity """ ) parser.add_argument( "-V", "--version", action="version", version=__version__, help=""" print the %(prog)s version number and exit """ ) parser.add_argument( "-e", "--ssh-cmd", metavar="CMD", default="ssh", help=""" the command to use to connect to the remote [%(default)s] """ ) parser.add_argument( "--seed-hosts", metavar="HOSTNAME[,HOSTNAME]", default=[], help=""" comma-separated list of hostnames for initial scan (may be used with or without --auto-hosts) """ ) parser.add_argument( "--no-latency-control", action="store_false", dest="latency_control", help=""" sacrifice latency to improve bandwidth benchmarks """ ) parser.add_argument( "--latency-buffer-size", metavar="SIZE", type=int, default=32768, dest="latency_buffer_size", help=""" size of latency control buffer """ ) parser.add_argument( "--wrap", metavar="NUM", type=int, help=""" restart counting channel numbers after this number (for testing) """ ) parser.add_argument( "--disable-ipv6", action="store_true", help=""" disable IPv6 support """ ) parser.add_argument( "-D", "--daemon", action="store_true", help=""" run in the background as a daemon """ ) parser.add_argument( "-s", "--subnets", metavar="PATH", action=Concat, dest="subnets_file", default=[], type=parse_subnetport_file, help=""" file where the subnets are stored, instead of on the command line """ ) parser.add_argument( "--syslog", action="store_true", help=""" send log messages to syslog (default if you use --daemon) """ ) parser.add_argument( "--pidfile", metavar="PATH", default="./sshuttle.pid", help=""" pidfile name (only if using --daemon) [%(default)s] """ ) parser.add_argument( "--user", help=""" apply all the rules only to this linux user """ ) parser.add_argument( "--firewall", action="store_true", help=""" (internal use only) """ ) parser.add_argument( "--hostwatch", action="store_true", help=""" (internal use only) """ ) parser.add_argument( "--sudoers", action="store_true", help=""" Add sshuttle to the sudoers for this user """ ) parser.add_argument( "--sudoers-no-modify", action="store_true", help=""" Prints the sudoers config to STDOUT and DOES NOT modify anything. """ ) parser.add_argument( "--sudoers-user", default="", help=""" Set the user name or group with %%group_name for passwordless operation. Default is the current user.set ALL for all users. Only works with --sudoers or --sudoers-no-modify option. """ ) parser.add_argument( "--sudoers-filename", default="sshuttle_auto", help=""" Set the file name for the sudoers.d file to be added. Default is "sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option. """ ) parser.add_argument( "--no-sudo-pythonpath", action="store_false", dest="sudo_pythonpath", help=""" do not set PYTHONPATH when invoking sudo """ ) parser.add_argument( "-t", "--tmark", metavar="[MARK]", default="1", help=""" transproxy optional traffic mark with provided MARK value """ ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/sshuttle/sdnotify.py0000600000175000017500000000317100000000000016417 0ustar00brianbrian"""When sshuttle is run via a systemd service file, we can communicate to systemd about the status of the sshuttle process. In particular, we can send READY status to tell systemd that sshuttle has completed startup and send STOPPING to indicate that sshuttle is beginning shutdown. For details, see: https://www.freedesktop.org/software/systemd/man/sd_notify.html """ import socket import os from sshuttle.helpers import debug1 def _notify(message): """Send a notification message to systemd.""" addr = os.environ.get("NOTIFY_SOCKET", None) if not addr or len(addr) == 1 or addr[0] not in ('/', '@'): return False addr = '\0' + addr[1:] if addr[0] == '@' else addr try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) except (OSError, IOError) as e: debug1("Error creating socket to notify systemd: %s\n" % e) return False if not message: return False assert isinstance(message, bytes) try: return (sock.sendto(message, addr) > 0) except (OSError, IOError) as e: debug1("Error notifying systemd: %s\n" % e) return False def send(*messages): """Send multiple messages to systemd.""" return _notify(b'\n'.join(messages)) def ready(): """Constructs a message that is appropriate to send upon completion of sshuttle startup.""" return b"READY=1" def stop(): """Constructs a message that is appropriate to send when sshuttle is beginning to shutdown.""" return b"STOPPING=1" def status(message): """Constructs a status message to be sent to systemd.""" return b"STATUS=%s" % message.encode('utf8') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/sshuttle/server.py0000600000175000017500000003034700000000000016073 0ustar00brianbrianimport re import struct import socket import traceback import time import sys import os import platform 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 b, log, debug1, debug2, debug3, Fatal, \ resolvconf_random_nameserver, which, get_env def _ipmatch(ipstr): # FIXME: IPv4 only 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) ips = ips return (struct.unpack('!I', socket.inet_aton(ips))[0], width) def _ipstr(ip, width): # FIXME: IPv4 only if width >= 32: return ip else: return "%s/%d" % (ip, width) def _maskbits(netmask): # FIXME: IPv4 only 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 _route_netstat(line): cols = line.split(None) if len(cols) < 3: return None, None ipw = _ipmatch(cols[0]) maskw = _ipmatch(cols[2]) # linux only mask = _maskbits(maskw) # returns 32 if maskw is null return ipw, mask def _route_iproute(line): ipm = line.split(None, 1)[0] if '/' not in ipm: return None, None ip, mask = ipm.split('/') ipw = _ipmatch(ip) return ipw, int(mask) def _list_routes(argv, extract_route): # FIXME: IPv4 only p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env()) routes = [] for line in p.stdout: if not line.strip(): continue ipw, mask = extract_route(line.decode("ASCII")) if not ipw: continue width = min(ipw[1], mask) 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)) return routes def list_routes(): if which('ip'): routes = _list_routes(['ip', 'route'], _route_iproute) elif which('netstat'): routes = _list_routes(['netstat', '-rn'], _route_netstat) else: log('WARNING: Neither "ip" nor "netstat" were found on the server. ' '--auto-nets feature will not work.\n') routes = [] for (family, ip, width) in 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, auto_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, auto_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, to_nameserver): Handler.__init__(self, []) self.timeout = time.time() + 30 self.mux = mux self.chan = chan self.tries = 0 self.request = request self.peers = {} self.to_ns_peer = None self.to_ns_port = None if to_nameserver is None: self.to_nameserver = None else: self.to_ns_peer, self.to_ns_port = to_nameserver.split("@") self.to_nameserver = self._addrinfo(self.to_ns_peer, self.to_ns_port) self.try_send() @staticmethod def _addrinfo(peer, port): if int(port) == 0: port = 53 family, _, _, _, sockaddr = socket.getaddrinfo(peer, port)[0] return (family, sockaddr) def try_send(self): if self.tries >= 3: return self.tries += 1 if self.to_nameserver is None: _, peer = resolvconf_random_nameserver(False) port = 53 else: peer = self.to_ns_peer port = int(self.to_ns_port) family, sockaddr = self._addrinfo(peer, port) sock = socket.socket(family, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63) sock.connect(sockaddr) self.peers[sock] = peer debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries)) try: sock.send(self.request) self.socks.append(sock) except socket.error: _, e = sys.exc_info()[:2] 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: _, e = sys.exc_info()[:2] 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, 63) def send(self, dstip, data): debug2(' s: UDP: sending to %r port %d\n' % dstip) try: self.sock.sendto(data, dstip) except socket.error: _, e = sys.exc_info()[:2] log(' s: 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: _, e = sys.exc_info()[:2] log(' s: UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e)) return debug2(' s: UDP response: %d bytes\n' % len(data)) hdr = b("%s,%r," % (peer[0], peer[1])) self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data) def main(latency_control, auto_hosts, to_nameserver, auto_nets): debug1(' s: Starting server with Python version %s\n' % platform.python_version()) helpers.logprefix = ' s: ' debug1('latency control setting = %r\n' % latency_control) # synchronization header sys.stdout.write('\0\0SSHUTTLE0001') sys.stdout.flush() handlers = [] mux = Mux(sys.stdin, sys.stdout) handlers.append(mux) debug1('auto-nets:' + str(auto_nets) + '\n') if auto_nets: routes = list(list_routes()) debug1('available routes:\n') for r in routes: debug1(' %d/%s/%d\n' % r) else: routes = [] routepkt = '' for r in routes: routepkt += '%d,%s,%d\n' % r mux.send(0, ssnet.CMD_ROUTES, b(routepkt)) hw = Hostwatch() hw.leftover = b('') def hostwatch_ready(sock): assert(hw.pid) content = hw.sock.recv(4096) if content: lines = (hw.leftover + content).split(b('\n')) if lines[-1]: # no terminating newline: entry isn't complete yet! hw.leftover = lines.pop() lines.append(b('')) else: hw.leftover = b('') mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines)) else: raise Fatal(' s: hostwatch process died') def got_host_req(data): if not hw.pid: (hw.pid, hw.sock) = start_hostwatch( data.decode("ASCII").strip().split(), auto_hosts) 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.decode("ASCII").split(',', 2) family = int(family) # AF_INET is the same constant on Linux and BSD but AF_INET6 # is different. As the client and server can be running on # different platforms we can not just set the socket family # to what comes in the wire. if family != socket.AF_INET: family = socket.AF_INET6 dstport = int(dstport) 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, to_nameserver) 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(b(','), 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(' s: 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() remove = [] for channel, h in dnshandlers.items(): if h.timeout < now or not h.ok: debug3('expiring dnsreqs channel=%d\n' % channel) remove.append(channel) h.ok = False for channel in remove: del dnshandlers[channel] if udphandlers: remove = [] for channel, h in udphandlers.items(): if not h.ok: debug3('expiring UDP channel=%d\n' % channel) remove.append(channel) h.ok = False for channel in remove: del udphandlers[channel] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/sshuttle/ssh.py0000600000175000017500000001252600000000000015361 0ustar00brianbrianimport sys import os import re import socket import zlib import importlib import importlib.util import subprocess as ssubprocess import shlex from shlex import quote import ipaddress from urllib.parse import urlparse import sshuttle.helpers as helpers from sshuttle.helpers import debug2, which, get_path, Fatal def get_module_source(name): spec = importlib.util.find_spec(name) with open(spec.origin, "rt") as f: return f.read().encode("utf-8") def empackage(z, name, data=None): if not data: data = get_module_source(name) content = z.compress(data) content += z.flush(zlib.Z_SYNC_FLUSH) return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content) def parse_hostport(rhostport): """ parses the given rhostport variable, looking like this: [username[:password]@]host[:port] if only host is given, can be a hostname, IPv4/v6 address or a ssh alias from ~/.ssh/config and returns a tuple (username, password, port, host) """ # leave use of default port to ssh command to prevent overwriting # ports configured in ~/.ssh/config when no port is given if rhostport is None or len(rhostport) == 0: return None, None, None, None port = None username = None password = None host = rhostport if "@" in host: # split username (and possible password) from the host[:port] username, host = host.rsplit("@", 1) # Fix #410 bad username error detect if ":" in username: # this will even allow for the username to be empty username, password = username.split(":") if ":" in host: # IPv6 address and/or got a port specified # If it is an IPv6 adress with port specification, # then it will look like: [::1]:22 try: # try to parse host as an IP adress, # if that works it is an IPv6 address host = str(ipaddress.ip_address(host)) except ValueError: # if that fails parse as URL to get the port parsed = urlparse('//{}'.format(host)) try: host = str(ipaddress.ip_address(parsed.hostname)) except ValueError: # else if both fails, we have a hostname with port host = parsed.hostname port = parsed.port if password is None or len(password) == 0: password = None return username, password, port, host def connect(ssh_cmd, rhostport, python, stderr, options): username, password, port, host = parse_hostport(rhostport) if username: rhost = "{}@{}".format(username, host) else: rhost = host z = zlib.compressobj(1) content = get_module_source('sshuttle.assembler') optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items())) optdata = optdata.encode("UTF8") content2 = (empackage(z, 'sshuttle') + 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, os; verbosity=%d; sys.stdin = os.fdopen(0, "rb"); 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.executable, '-c', pyscript] else: if ssh_cmd: sshl = shlex.split(ssh_cmd) else: sshl = ['ssh'] if port is not None: portl = ["-p", str(port)] else: portl = [] if python: pycmd = "'%s' -c '%s'" % (python, pyscript) else: pycmd = ("P=python3; $P -V 2>%s || P=python; " "exec \"$P\" -c %s") % (os.devnull, quote(pyscript)) pycmd = ("/bin/sh -c {}".format(quote(pycmd))) if password is not None: os.environ['SSHPASS'] = str(password) argv = (["sshpass", "-e"] + sshl + portl + [rhost, '--', pycmd]) else: argv = (sshl + portl + [rhost, '--', pycmd]) # Our which() function searches for programs in get_path() # directories (which include PATH). This step isn't strictly # necessary if ssh is already in the user's PATH, but it makes the # error message friendlier if the user incorrectly passes in a # custom ssh command that we cannot find. abs_path = which(argv[0]) if abs_path is None: raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path())) argv[0] = abs_path (s1, s2) = socket.socketpair() def setup(): # 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109731.0 sshuttle-1.0.5/sshuttle/ssnet.py0000600000175000017500000004646400000000000015730 0ustar00brianbrianimport sys import struct import socket import errno import select import os import fcntl from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal MAX_CHANNEL = 65535 LATENCY_BUFFER_SIZE = 32768 SHUT_RD = 0 SHUT_WR = 1 SHUT_RDWR = 2 HDR_LEN = 8 CMD_EXIT = 0x4200 CMD_PING = 0x4201 CMD_PONG = 0x4202 CMD_TCP_CONNECT = 0x4203 CMD_TCP_STOP_SENDING = 0x4204 CMD_TCP_EOF = 0x4205 CMD_TCP_DATA = 0x4206 CMD_ROUTES = 0x4207 CMD_HOST_REQ = 0x4208 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_EXIT: 'EXIT', CMD_PING: 'PING', CMD_PONG: 'PONG', CMD_TCP_CONNECT: 'TCP_CONNECT', CMD_TCP_STOP_SENDING: 'TCP_STOP_SENDING', CMD_TCP_EOF: 'TCP_EOF', CMD_TCP_DATA: 'TCP_DATA', CMD_ROUTES: 'ROUTES', CMD_HOST_REQ: 'HOST_REQ', 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, errno.ENETUNREACH, errno.ECONNABORTED, errno.ECONNRESET] def _add(socks, elem): if elem not in socks: socks.append(elem) def _fds(socks): out = [] for i in socks: try: out.append(i.fileno()) except AttributeError: out.append(i) out.sort() return out def _nb_clean(func, *args): try: return func(*args) except OSError: _, e = sys.exc_info()[:2] if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN): raise else: debug3('%s: err was: %s\n' % (func.__name__, e)) return None def _try_peername(sock): try: pn = sock.getpeername() if pn: return '%s:%s' % (pn[0], pn[1]) except socket.error: _, e = sys.exc_info()[:2] if e.args[0] == errno.EINVAL: pass elif e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK): raise except AttributeError: pass return 'unknown' _swcount = 0 class SockWrapper: def __init__(self, rsock, wsock, connect_to=None, peername=None): global _swcount _swcount += 1 debug3('creating new SockWrapper (%d now exist)\n' % _swcount) self.exc = None self.rsock = rsock self.wsock = wsock self.shut_read = self.shut_write = False self.buf = [] self.connect_to = connect_to self.peername = peername or _try_peername(self.rsock) self.try_connect() def __del__(self): global _swcount _swcount -= 1 debug1('%r: deleting (%d remain)\n' % (self, _swcount)) if self.exc: debug1('%r: error was: %s\n' % (self, self.exc)) def __repr__(self): 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): if not self.exc: self.exc = e self.nowrite() self.noread() def try_connect(self): if self.connect_to and self.shut_write: self.noread() self.connect_to = None if not self.connect_to: return # already connected self.rsock.setblocking(False) debug3('%r: trying connect to %r\n' % (self, self.connect_to)) try: self.rsock.connect(self.connect_to) # connected successfully (Linux) self.connect_to = None except socket.error: _, e = sys.exc_info()[:2] debug3('%r: connect result: %s\n' % (self, e)) 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]: 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: # connected successfully (BSD) self.connect_to = None elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]: # a "normal" kind of error self.connect_to = None self.seterr(e) else: raise # error we've never heard of?! barf completely. def noread(self): if not self.shut_read: debug2('%r: done reading\n' % self) self.shut_read = True # self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway def nowrite(self): if not self.shut_write: debug2('%r: done writing\n' % self) self.shut_write = True try: self.wsock.shutdown(SHUT_WR) except socket.error: _, e = sys.exc_info()[:2] self.seterr('nowrite: %s' % e) @staticmethod def too_full(): return False # fullness is determined by the socket's select() state def uwrite(self, buf): if self.connect_to: return 0 # still connecting self.wsock.setblocking(False) try: return _nb_clean(os.write, self.wsock.fileno(), buf) except OSError: _, e = sys.exc_info()[:2] if e.errno == errno.EPIPE: debug1('%r: uwrite: got EPIPE\n' % self) self.nowrite() return 0 else: # unexpected error... stream is dead self.seterr('uwrite: %s' % e) return 0 def write(self, buf): assert(buf) return self.uwrite(buf) def uread(self): if self.connect_to: return None # still connecting if self.shut_read: return self.rsock.setblocking(False) try: return _nb_clean(os.read, self.rsock.fileno(), 65536) except OSError: _, e = sys.exc_info()[:2] self.seterr('uread: %s' % e) return b('') # unexpected error... we'll call it EOF def fill(self): if self.buf: return rb = self.uread() if rb: self.buf.append(rb) if rb == b(''): # empty string means EOF; None means temporarily empty self.noread() def copy_to(self, outwrap): if self.buf and self.buf[0]: wrote = outwrap.write(self.buf[0]) self.buf[0] = self.buf[0][wrote:] while self.buf and not self.buf[0]: self.buf.pop(0) if not self.buf and self.shut_read: outwrap.nowrite() class Handler: def __init__(self, socks=None, callback=None): self.ok = True self.socks = socks or [] if callback: self.callback = callback def pre_select(self, r, w, x): for i in self.socks: _add(r, i) def callback(self, sock): log('--no callback defined-- %r\n' % self) (r, _, _) = select.select(self.socks, [], [], 0) for s in r: v = s.recv(4096) if not v: log('--closed-- %r\n' % self) self.socks = [] self.ok = False class Proxy(Handler): def __init__(self, wrap1, wrap2): Handler.__init__(self, [wrap1.rsock, wrap1.wsock, wrap2.rsock, wrap2.wsock]) self.wrap1 = wrap1 self.wrap2 = wrap2 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: _add(w, self.wrap1.rsock) elif self.wrap1.buf: if not self.wrap2.too_full(): _add(w, self.wrap2.wsock) elif not self.wrap1.shut_read: _add(r, self.wrap1.rsock) if self.wrap2.connect_to: _add(w, self.wrap2.rsock) elif self.wrap2.buf: if not self.wrap1.too_full(): _add(w, self.wrap1.wsock) elif not self.wrap2.shut_read: _add(r, self.wrap2.rsock) def callback(self, sock): self.wrap1.try_connect() self.wrap2.try_connect() self.wrap1.fill() self.wrap2.fill() self.wrap1.copy_to(self.wrap2) 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 not self.wrap1.buf and not self.wrap2.buf): self.ok = False self.wrap1.nowrite() self.wrap2.nowrite() class Mux(Handler): def __init__(self, rfile, wfile): Handler.__init__(self, [rfile, wfile]) self.rfile = rfile self.wfile = wfile self.new_channel = self.got_dns_req = self.got_routes = None self.got_udp_open = self.got_udp_data = self.got_udp_close = None self.got_host_req = self.got_host_list = None self.channels = {} self.chani = 0 self.want = 0 self.inbuf = b('') self.outbuf = [] self.fullness = 0 self.too_full = False self.send(0, CMD_PING, b('chicken')) def next_channel(self): # channel 0 is special, so we never allocate it for _ in range(1024): self.chani += 1 if self.chani > MAX_CHANNEL: self.chani = 1 if not self.channels.get(self.chani): return self.chani def amount_queued(self): total = 0 for byte in self.outbuf: total += len(byte) return total def check_fullness(self): if self.fullness > LATENCY_BUFFER_SIZE: if not self.too_full: self.send(0, CMD_PING, b('rttest')) self.too_full = True # ob = [] # for b in self.outbuf: # (s1,s2,c) = struct.unpack('!ccH', b[:4]) # ob.append(c) # log('outbuf: %d %r\n' % (self.amount_queued(), ob)) def send(self, channel, cmd, data): assert isinstance(data, bytes) assert len(data) <= 65535 p = struct.pack('!ccHHH', b('S'), b('S'), channel, cmd, len(data)) \ + data self.outbuf.append(p) debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n' % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data), self.fullness)) self.fullness += len(data) def got_packet(self, channel, cmd, data): debug2('< channel=%d cmd=%s len=%d\n' % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data))) if cmd == CMD_PING: self.send(0, CMD_PONG, data) elif cmd == CMD_PONG: debug2('received PING response\n') self.too_full = False self.fullness = 0 elif cmd == CMD_EXIT: self.ok = False elif cmd == CMD_TCP_CONNECT: assert(not self.channels.get(channel)) if self.new_channel: 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: if self.got_routes: self.got_routes(data) else: raise Exception('got CMD_ROUTES without got_routes?') elif cmd == CMD_HOST_REQ: if self.got_host_req: self.got_host_req(data) else: raise Exception('got CMD_HOST_REQ without got_host_req?') elif cmd == CMD_HOST_LIST: if self.got_host_list: self.got_host_list(data) else: raise Exception('got CMD_HOST_LIST without got_host_list?') else: callback = self.channels.get(channel) 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): try: os.set_blocking(self.wfile.fileno(), False) except AttributeError: # python < 3.5 flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_GETFL) flags |= os.O_NONBLOCK flags = fcntl.fcntl(self.wfile.fileno(), fcntl.F_SETFL, flags) if self.outbuf and self.outbuf[0]: wrote = _nb_clean(os.write, self.wfile.fileno(), self.outbuf[0]) debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0]))) if wrote: self.outbuf[0] = self.outbuf[0][wrote:] while self.outbuf and not self.outbuf[0]: self.outbuf[0:1] = [] def fill(self): try: os.set_blocking(self.rfile.fileno(), False) except AttributeError: # python < 3.5 flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL) flags |= os.O_NONBLOCK flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags) try: read = _nb_clean(os.read, self.rfile.fileno(), LATENCY_BUFFER_SIZE) except OSError: _, e = sys.exc_info()[:2] raise Fatal('other end: %r' % e) # log('<<< %r\n' % b) if read == b(''): # EOF self.ok = False if read: self.inbuf += read def handle(self): self.fill() # log('inbuf is: (%d,%d) %r\n' # % (self.want, len(self.inbuf), self.inbuf)) while 1: if len(self.inbuf) >= (self.want or HDR_LEN): (s1, s2, channel, cmd, datalen) = \ struct.unpack('!ccHHH', self.inbuf[:HDR_LEN]) assert(s1 == b('S')) assert(s2 == b('S')) self.want = datalen + HDR_LEN if self.want and len(self.inbuf) >= self.want: data = self.inbuf[HDR_LEN:self.want] self.inbuf = self.inbuf[self.want:] self.want = 0 self.got_packet(channel, cmd, data) else: break def pre_select(self, r, w, x): _add(r, self.rfile) if self.outbuf: _add(w, self.wfile) def callback(self, sock): (r, w, _) = select.select([self.rfile], [self.wfile], [], 0) if self.rfile in r: self.handle() if self.outbuf and self.wfile in w: self.flush() class MuxWrapper(SockWrapper): def __init__(self, mux, channel): SockWrapper.__init__(self, mux.rfile, mux.wfile) self.mux = mux self.channel = channel self.mux.channels[channel] = self.got_packet self.socks = [] debug2('new channel: %d\n' % channel) def __del__(self): self.nowrite() SockWrapper.__del__(self) def __repr__(self): return 'SW%r:Mux#%d' % (self.peername, self.channel) def noread(self): if not self.shut_read: self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b('')) self.setnoread() def setnoread(self): if not self.shut_read: debug2('%r: done reading\n' % self) self.shut_read = True self.maybe_close() def nowrite(self): if not self.shut_write: self.mux.send(self.channel, CMD_TCP_EOF, b('')) self.setnowrite() def setnowrite(self): if not self.shut_write: debug2('%r: done writing\n' % self) self.shut_write = True self.maybe_close() def maybe_close(self): if self.shut_read and self.shut_write: debug2('%r: closing connection\n' % self) # 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): return self.mux.too_full def uwrite(self, buf): if self.mux.too_full: return 0 # too much already enqueued if len(buf) > 2048: buf = buf[:2048] self.mux.send(self.channel, CMD_TCP_DATA, buf) return len(buf) def uread(self): if self.shut_read: return b('') # EOF else: return None # no data available right now def got_packet(self, cmd, data): if cmd == CMD_TCP_EOF: # Remote side already knows the status - set flag but don't notify self.setnoread() elif cmd == CMD_TCP_STOP_SENDING: # Remote side already knows the status - set flag but don't notify self.setnowrite() elif cmd == CMD_TCP_DATA: self.buf.append(data) else: raise Exception('unknown command %d (%d bytes)' % (cmd, len(data))) def connect_dst(family, ip, port): debug2('Connecting to %s:%d\n' % (ip, port)) outsock = socket.socket(family) outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63) return SockWrapper(outsock, outsock, connect_to=(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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1548627954.0 sshuttle-1.0.5/sshuttle/ssyslog.py0000600000175000017500000000111400000000000016256 0ustar00brianbrianimport sys import os import subprocess as ssubprocess _p = None def start_syslog(): global _p with open(os.devnull, 'w') as devnull: _p = ssubprocess.Popen( ['logger', '-p', 'daemon.notice', '-t', 'sshuttle'], stdin=ssubprocess.PIPE, stdout=devnull, stderr=devnull ) def close_stdin(): sys.stdin.close() def stdout_to_syslog(): sys.stdout.flush() os.dup2(_p.stdin.fileno(), sys.stdout.fileno()) def stderr_to_syslog(): sys.stderr.flush() os.dup2(_p.stdin.fileno(), sys.stderr.fileno()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109731.0 sshuttle-1.0.5/sshuttle/stresstest.py0000700000175000017500000000503400000000000017004 0ustar00brianbrian#!/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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109731.0 sshuttle-1.0.5/sshuttle/sudoers.py0000600000175000017500000000304500000000000016244 0ustar00brianbrianimport os import sys import getpass from uuid import uuid4 from subprocess import Popen, PIPE from sshuttle.helpers import log, debug1 from distutils import spawn path_to_sshuttle = sys.argv[0] path_to_dist_packages = os.path.dirname(os.path.abspath(__file__))[:-9] # randomize command alias to avoid collisions command_alias = 'SSHUTTLE%(num)s' % {'num': uuid4().hex[-3:].upper()} # Template for the sudoers file template = ''' Cmnd_Alias %(ca)s = /usr/bin/env PYTHONPATH=%(dist_packages)s %(py)s %(path)s * %(user_name)s ALL=NOPASSWD: %(ca)s ''' def build_config(user_name): content = template % { 'ca': command_alias, 'dist_packages': path_to_dist_packages, 'py': sys.executable, 'path': path_to_sshuttle, 'user_name': user_name, } return content def save_config(content, file_name): process = Popen([ '/usr/bin/sudo', spawn.find_executable('sudoers-add'), file_name, ], stdout=PIPE, stdin=PIPE) process.stdin.write(content.encode()) streamdata = process.communicate()[0] returncode = process.returncode if returncode: log('Failed updating sudoers file.\n') debug1(streamdata) exit(returncode) else: log('Success, sudoers file update.\n') exit(0) def sudoers(user_name=None, no_modify=None, file_name=None): user_name = user_name or getpass.getuser() content = build_config(user_name) if no_modify: sys.stdout.write(content) exit(0) else: save_config(content, file_name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198743.0 sshuttle-1.0.5/sshuttle/version.py0000600000175000017500000000021600000000000016242 0ustar00brianbrian# coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control version = '1.0.5' version_tuple = (1, 0, 5) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/sshuttle.egg-info/0000700000175000017500000000000000000000000015674 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198743.0 sshuttle-1.0.5/sshuttle.egg-info/PKG-INFO0000600000175000017500000001017200000000000016774 0ustar00brianbrianMetadata-Version: 2.1 Name: sshuttle Version: 1.0.5 Summary: Full-featured" VPN over an SSH tunnel Home-page: https://github.com/sshuttle/sshuttle Author: Brian May Author-email: brian@linuxpenguins.xyz License: LGPL2.1+ Description: 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`_. .. _terrible performance: https://sshuttle.readthedocs.io/en/stable/how-it-works.html Obtaining sshuttle ------------------ - Ubuntu 16.04 or later:: apt-get install sshuttle - Debian stretch or later:: apt-get install sshuttle - Arch Linux:: pacman -S sshuttle - Fedora:: dnf install sshuttle - NixOS:: nix-env -iA nixos.sshuttle - From PyPI:: sudo pip install sshuttle - Clone:: git clone https://github.com/sshuttle/sshuttle.git cd sshuttle sudo ./setup.py install - FreeBSD:: # ports cd /usr/ports/net/py-sshuttle && make install clean # pkg pkg install py36-sshuttle It is also possible to install into a virtualenv as a non-root user. - From PyPI:: virtualenv -p python3 /tmp/sshuttle . /tmp/sshuttle/bin/activate pip install sshuttle - Clone:: virtualenv -p python3 /tmp/sshuttle . /tmp/sshuttle/bin/activate git clone https://github.com/sshuttle/sshuttle.git cd sshuttle ./setup.py install - Homebrew:: brew install sshuttle - Nix:: nix-env -iA nixpkgs.sshuttle Documentation ------------- The documentation for the stable version is available at: https://sshuttle.readthedocs.org/ The documentation for the latest development version is available at: https://sshuttle.readthedocs.org/en/latest/ Running as a service -------------------- Sshuttle can also be run as a service and configured using a config management system: https://medium.com/@mike.reider/using-sshuttle-as-a-service-bec2684a65fe Keywords: ssh vpn Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: System :: Networking Requires-Python: >=3.6 Description-Content-Type: text/x-rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198743.0 sshuttle-1.0.5/sshuttle.egg-info/SOURCES.txt0000600000175000017500000000273200000000000017566 0ustar00brianbrian.gitignore .prospector.yml CHANGES.rst LICENSE MANIFEST.in README.rst bandit.yml requirements-tests.txt requirements.txt run setup.cfg setup.py tox.ini .github/workflows/pythonpackage.yml bin/sudoers-add docs/Makefile docs/changes.rst docs/chromeos.rst docs/conf.py docs/how-it-works.rst docs/index.rst docs/installation.rst docs/make.bat docs/manpage.rst docs/openwrt.rst docs/overview.rst docs/platform.rst docs/requirements.rst docs/support.rst docs/tproxy.rst docs/trivia.rst docs/usage.rst docs/windows.rst sshuttle/__init__.py sshuttle/__main__.py sshuttle/assembler.py sshuttle/client.py sshuttle/cmdline.py sshuttle/firewall.py sshuttle/helpers.py sshuttle/hostwatch.py sshuttle/linux.py sshuttle/options.py sshuttle/sdnotify.py sshuttle/server.py sshuttle/ssh.py sshuttle/ssnet.py sshuttle/ssyslog.py sshuttle/stresstest.py sshuttle/sudoers.py sshuttle/version.py sshuttle.egg-info/PKG-INFO sshuttle.egg-info/SOURCES.txt sshuttle.egg-info/dependency_links.txt sshuttle.egg-info/entry_points.txt sshuttle.egg-info/requires.txt sshuttle.egg-info/top_level.txt sshuttle/methods/__init__.py sshuttle/methods/ipfw.py sshuttle/methods/nat.py sshuttle/methods/nft.py sshuttle/methods/pf.py sshuttle/methods/tproxy.py tests/client/test_firewall.py tests/client/test_helpers.py tests/client/test_methods_nat.py tests/client/test_methods_pf.py tests/client/test_methods_tproxy.py tests/client/test_options.py tests/client/test_sdnotify.py tests/server/test_server.py tests/ssh/test_parse_hostport.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198743.0 sshuttle-1.0.5/sshuttle.egg-info/dependency_links.txt0000600000175000017500000000000100000000000021744 0ustar00brianbrian ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198743.0 sshuttle-1.0.5/sshuttle.egg-info/entry_points.txt0000600000175000017500000000006400000000000021174 0ustar00brianbrian[console_scripts] sshuttle = sshuttle.cmdline:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198743.0 sshuttle-1.0.5/sshuttle.egg-info/requires.txt0000600000175000017500000000000700000000000020273 0ustar00brianbrianpsutil ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609198743.0 sshuttle-1.0.5/sshuttle.egg-info/top_level.txt0000600000175000017500000000001100000000000020420 0ustar00brianbriansshuttle ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9785907 sshuttle-1.0.5/tests/0000700000175000017500000000000000000000000013471 5ustar00brianbrian././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/tests/client/0000700000175000017500000000000000000000000014747 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609112797.0 sshuttle-1.0.5/tests/client/test_firewall.py0000600000175000017500000001046600000000000020176 0ustar00brianbrianimport io from socket import AF_INET, AF_INET6 from mock import Mock, patch, call import sshuttle.firewall def setup_daemon(): stdin = io.StringIO(u"""ROUTES {inet},24,0,1.2.3.0,8000,9000 {inet},32,1,1.2.3.66,8080,8080 {inet6},64,0,2404:6800:4004:80c::,0,0 {inet6},128,1,2404:6800:4004:80c::101f,80,80 NSLIST {inet},1.2.3.33 {inet6},2404:6800:4004:80c::33 PORTS 1024,1025,1026,1027 GO 1 - HOST 1.2.3.3,existing """.format(inet=AF_INET, inet6=AF_INET6)) stdout = Mock() return stdin, stdout def test_rewrite_etc_hosts(tmpdir): orig_hosts = tmpdir.join("hosts.orig") orig_hosts.write("1.2.3.3 existing\n") new_hosts = tmpdir.join("hosts") orig_hosts.copy(new_hosts) hostmap = { 'myhost': '1.2.3.4', 'myotherhost': '1.2.3.5', } with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)): sshuttle.firewall.rewrite_etc_hosts(hostmap, 10) with new_hosts.open() 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 == "" with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)): sshuttle.firewall.restore_etc_hosts(hostmap, 10) assert orig_hosts.computehash() == new_hosts.computehash() def test_subnet_weight(): subnets = [ (AF_INET, 16, 0, '192.168.0.0', 0, 0), (AF_INET, 24, 0, '192.168.69.0', 0, 0), (AF_INET, 32, 0, '192.168.69.70', 0, 0), (AF_INET, 32, 1, '192.168.69.70', 0, 0), (AF_INET, 32, 1, '192.168.69.70', 80, 80), (AF_INET, 0, 1, '0.0.0.0', 0, 0), (AF_INET, 0, 1, '0.0.0.0', 8000, 9000), (AF_INET, 0, 1, '0.0.0.0', 8000, 8500), (AF_INET, 0, 1, '0.0.0.0', 8000, 8000), (AF_INET, 0, 1, '0.0.0.0', 400, 450) ] subnets_sorted = [ (AF_INET, 32, 1, '192.168.69.70', 80, 80), (AF_INET, 0, 1, '0.0.0.0', 8000, 8000), (AF_INET, 0, 1, '0.0.0.0', 400, 450), (AF_INET, 0, 1, '0.0.0.0', 8000, 8500), (AF_INET, 0, 1, '0.0.0.0', 8000, 9000), (AF_INET, 32, 1, '192.168.69.70', 0, 0), (AF_INET, 32, 0, '192.168.69.70', 0, 0), (AF_INET, 24, 0, '192.168.69.0', 0, 0), (AF_INET, 16, 0, '192.168.0.0', 0, 0), (AF_INET, 0, 1, '0.0.0.0', 0, 0) ] assert subnets_sorted == sorted(subnets, key=sshuttle.firewall.subnet_weight, reverse=True) @patch('sshuttle.firewall.rewrite_etc_hosts') @patch('sshuttle.firewall.setup_daemon') @patch('sshuttle.firewall.get_method') def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): stdin, stdout = setup_daemon() mock_setup_daemon.return_value = stdin, stdout mock_get_method("not_auto").name = "test" mock_get_method.reset_mock() sshuttle.firewall.main("not_auto", False) assert mock_rewrite_etc_hosts.mock_calls == [ call({'1.2.3.3': 'existing'}, 1024), call({}, 1024), ] assert stdout.mock_calls == [ call.write('READY test\n'), call.flush(), call.write('STARTED\n'), call.flush() ] assert mock_setup_daemon.mock_calls == [call()] assert mock_get_method.mock_calls == [ call('not_auto'), call().is_supported(), call().is_supported().__bool__(), call().setup_firewall( 1024, 1026, [(AF_INET6, u'2404:6800:4004:80c::33')], AF_INET6, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, None), call().setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, None), call().restore_firewall(1024, AF_INET6, True, None), call().restore_firewall(1025, AF_INET, True, None), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/tests/client/test_helpers.py0000600000175000017500000001407500000000000020033 0ustar00brianbrianimport io import socket from socket import AF_INET, AF_INET6 import errno from mock import patch, call 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") sshuttle.helpers.log("abc") sshuttle.helpers.log("message 1\n") sshuttle.helpers.log("message 2\nline2\nline3\n") sshuttle.helpers.log("message 3\nline2\nline3") assert mock_stdout.mock_calls == [ call.flush(), call.flush(), call.flush(), call.flush(), call.flush(), ] assert mock_stderr.mock_calls == [ call.write('prefix: message'), call.flush(), call.write('prefix: abc'), call.flush(), call.write('prefix: message 1\n'), call.flush(), call.write('prefix: message 2\n'), call.write('---> line2\n'), call.write('---> line3\n'), call.flush(), call.write('prefix: message 3\n'), call.write('---> line2\n'), call.write('---> line3\n'), 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(False) assert ns == [ (AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'), (AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'), (AF_INET6, u'2404:6800:4004:80c::1'), (AF_INET6, u'2404:6800:4004:80c::2'), (AF_INET6, u'2404:6800:4004:80c::3'), (AF_INET6, 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(False) assert ns in [ (AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'), (AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'), (AF_INET6, u'2404:6800:4004:80c::1'), (AF_INET6, u'2404:6800:4004:80c::2'), (AF_INET6, u'2404:6800:4004:80c::3'), (AF_INET6, u'2404:6800:4004:80c::4') ] @patch('sshuttle.helpers.socket.socket.bind') def test_islocal(mock_bind): bind_error = socket.error(errno.EADDRNOTAVAIL) mock_bind.side_effect = [None, bind_error, None, bind_error] assert sshuttle.helpers.islocal("127.0.0.1", AF_INET) assert not sshuttle.helpers.islocal("192.0.2.1", AF_INET) assert sshuttle.helpers.islocal("::1", AF_INET6) assert not sshuttle.helpers.islocal("2001:db8::1", AF_INET6) def test_family_ip_tuple(): assert sshuttle.helpers.family_ip_tuple("127.0.0.1") \ == (AF_INET, "127.0.0.1") assert sshuttle.helpers.family_ip_tuple("192.168.2.6") \ == (AF_INET, "192.168.2.6") assert sshuttle.helpers.family_ip_tuple("::1") \ == (AF_INET6, "::1") assert sshuttle.helpers.family_ip_tuple("2404:6800:4004:80c::1") \ == (AF_INET6, "2404:6800:4004:80c::1") def test_family_to_string(): assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET" assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6" expected = 'AddressFamily.AF_UNIX' assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/tests/client/test_methods_nat.py0000600000175000017500000001367400000000000020702 0ustar00brianbrianimport socket from socket import AF_INET, AF_INET6 import struct import pytest from mock import Mock, patch, call from sshuttle.helpers import Fatal 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 assert features.dns def test_get_tcp_dstip(): sock = Mock() sock.family = AF_INET sock.getsockopt.return_value = struct.pack( '!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1) method = get_method('nat') assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024) assert sock.mock_calls == [call.getsockopt(0, 80, 16)] sock = Mock() sock.family = AF_INET6 sock.getsockopt.return_value = struct.pack( '!HH4xBBBBBBBBBBBBBBBB', socket.ntohs(AF_INET6), 1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1) method = get_method('nft') assert method.get_tcp_dstip(sock) == ('::1', 1024) assert sock.mock_calls == [call.getsockopt(41, 80, 64)] def test_recv_udp(): sock = Mock() 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_assert_features(): method = get_method('nat') features = method.get_supported_features() method.assert_features(features) features.udp = True with pytest.raises(Fatal): method.assert_features(features) features.ipv6 = True with pytest.raises(Fatal): method.assert_features(features) 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, [(AF_INET6, u'2404:6800:4004:80c::33')], AF_INET6, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], True, None) assert str(excinfo.value) \ == 'Address family "AF_INET6" unsupported by nat method_name' 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, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], True, None) assert str(excinfo.value) == 'UDP not supported by nat method_name' assert mock_ipt_chain_exists.mock_calls == [] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [] method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], False, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'nat', 'sshuttle-1025') ] assert mock_ipt_ttl.mock_calls == [ call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', '-m', 'ttl', '--ttl', '63') ] assert mock_ipt.mock_calls == [ call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-F', 'sshuttle-1025'), call(AF_INET, 'nat', '-X', 'sshuttle-1025'), call(AF_INET, 'nat', '-N', 'sshuttle-1025'), call(AF_INET, 'nat', '-F', 'sshuttle-1025'), call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', '--dest', u'1.2.3.33/32', '-p', 'udp', '--dport', '53', '--to-ports', '1027'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', '-m', 'addrtype', '--dst-type', 'LOCAL'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', '--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000', '--to-ports', '1025') ] mock_ipt_chain_exists.reset_mock() mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() method.restore_firewall(1025, AF_INET, False, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'nat', 'sshuttle-1025') ] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [ call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-F', 'sshuttle-1025'), call(AF_INET, 'nat', '-X', 'sshuttle-1025') ] mock_ipt_chain_exists.reset_mock() mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/tests/client/test_methods_pf.py0000600000175000017500000004112500000000000020515 0ustar00brianbrianimport socket from socket import AF_INET, AF_INET6 import pytest from mock import Mock, patch, call, ANY from sshuttle.methods import get_method from sshuttle.helpers import Fatal, get_env from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd def test_get_supported_features(): method = get_method('pf') features = method.get_supported_features() assert features.ipv6 assert not features.udp assert features.dns @patch('sshuttle.helpers.verbose', new=3) def test_get_tcp_dstip(): sock = Mock() sock.getpeername.return_value = ("127.0.0.1", 1024) sock.getsockname.return_value = ("127.0.0.2", 1025) sock.family = AF_INET firewall = Mock() firewall.pfile.readline.return_value = \ b"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n" method = get_method('pf') method.set_firewall(firewall) assert method.get_tcp_dstip(sock) == ('127.0.0.3', 1026) assert sock.mock_calls == [ call.getpeername(), call.getsockname(), ] assert firewall.mock_calls == [ call.pfile.write(b'QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'), call.pfile.flush(), call.pfile.readline() ] def test_recv_udp(): sock = Mock() sock.recvfrom.return_value = "11111", "127.0.0.1" method = get_method('pf') result = method.recv_udp(sock, 1024) assert sock.mock_calls == [call.recvfrom(1024)] assert result == ("127.0.0.1", None, "11111") def test_send_udp(): sock = Mock() method = get_method('pf') method.send_udp(sock, None, "127.0.0.1", "22222") assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")] def test_setup_tcp_listener(): listener = Mock() method = get_method('pf') method.setup_tcp_listener(listener) assert listener.mock_calls == [] def test_setup_udp_listener(): listener = Mock() method = get_method('pf') method.setup_udp_listener(listener) assert listener.mock_calls == [] def test_assert_features(): method = get_method('pf') features = method.get_supported_features() method.assert_features(features) features.udp = True with pytest.raises(Fatal): method.assert_features(features) features.ipv6 = True with pytest.raises(Fatal): method.assert_features(features) @patch('sshuttle.methods.pf.pf', Darwin()) @patch('sshuttle.methods.pf.sys.stdout') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout): method = get_method('pf') assert not method.firewall_command("somthing") command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( AF_INET, socket.IPPROTO_TCP, "127.0.0.1", 1025, "127.0.0.2", 1024) assert method.firewall_command(command) assert mock_pf_get_dev.mock_calls == [call()] assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xc0544417, ANY), ] assert mock_stdout.mock_calls == [ call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'), call.flush(), ] @patch('sshuttle.methods.pf.pf', FreeBsd()) @patch('sshuttle.methods.pf.sys.stdout') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') def test_firewall_command_freebsd(mock_pf_get_dev, mock_ioctl, mock_stdout): method = get_method('pf') assert not method.firewall_command("somthing") command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( AF_INET, socket.IPPROTO_TCP, "127.0.0.1", 1025, "127.0.0.2", 1024) assert method.firewall_command(command) assert mock_pf_get_dev.mock_calls == [call()] assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xc04c4417, ANY), ] assert mock_stdout.mock_calls == [ call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'), call.flush(), ] @patch('sshuttle.methods.pf.pf', OpenBsd()) @patch('sshuttle.methods.pf.sys.stdout') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout): method = get_method('pf') assert not method.firewall_command("somthing") command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( AF_INET, socket.IPPROTO_TCP, "127.0.0.1", 1025, "127.0.0.2", 1024) assert method.firewall_command(command) assert mock_pf_get_dev.mock_calls == [call()] assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xc0504417, ANY), ] assert mock_stdout.mock_calls == [ call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'), call.flush(), ] def pfctl(args, stdin=None): if args == '-s Interfaces -i lo -v': return (b'lo0 (skip)',) if args == '-s all': return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n', b'little lamb\n') if args == '-E': return (b'\n', b'Token : abcdefg\n') return None @patch('sshuttle.helpers.verbose', new=3) @patch('sshuttle.methods.pf.pf', Darwin()) @patch('sshuttle.methods.pf.pfctl') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): mock_pfctl.side_effect = pfctl method = get_method('pf') assert method.name == 'pf' # IPV6 method.setup_firewall( 1024, 1026, [(AF_INET6, u'2404:6800:4004:80c::33')], AF_INET6, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), ] assert mock_pfctl.mock_calls == [ call('-s Interfaces -i lo -v'), call('-f /dev/stdin', b'pass on lo\n'), call('-s all'), call('-a sshuttle6-1024 -f /dev/stdin', b'table {2404:6800:4004:80c::33}\n' b'rdr pass on lo0 inet6 proto tcp from ! ::1 to ' b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n' b'rdr pass on lo0 inet6 proto udp ' b'to port 53 -> ::1 port 1026\n' b'pass out route-to lo0 inet6 proto tcp to ' b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n' b'pass out inet6 proto tcp to ' b'2404:6800:4004:80c::101f/128 port 8080:8080\n' b'pass out route-to lo0 inet6 proto udp ' b'to port 53 keep state\n'), call('-E'), ] mock_pf_get_dev.reset_mock() mock_ioctl.reset_mock() mock_pfctl.reset_mock() with pytest.raises(Exception) as excinfo: method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None) assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [] method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY), ] assert mock_pfctl.mock_calls == [ call('-s Interfaces -i lo -v'), call('-f /dev/stdin', b'pass on lo\n'), call('-s all'), call('-a sshuttle-1025 -f /dev/stdin', b'table {1.2.3.33}\n' b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 to 1.2.3.0/24 ' b'-> 127.0.0.1 port 1025\n' b'rdr pass on lo0 inet proto udp ' b'to port 53 -> 127.0.0.1 port 1027\n' b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n' b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n' b'pass out route-to lo0 inet proto udp ' b'to port 53 keep state\n'), call('-E'), ] mock_pf_get_dev.reset_mock() mock_ioctl.reset_mock() mock_pfctl.reset_mock() method.restore_firewall(1025, AF_INET, False, None) assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [ call('-a sshuttle-1025 -F all'), call("-X abcdefg"), ] mock_pf_get_dev.reset_mock() mock_pfctl.reset_mock() mock_ioctl.reset_mock() @patch('sshuttle.helpers.verbose', new=3) @patch('sshuttle.methods.pf.pf', FreeBsd()) @patch('subprocess.call') @patch('sshuttle.methods.pf.pfctl') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl, mock_subprocess_call): mock_pfctl.side_effect = pfctl method = get_method('pf') assert method.name == 'pf' method.setup_firewall( 1024, 1026, [(AF_INET6, u'2404:6800:4004:80c::33')], AF_INET6, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None) assert mock_pfctl.mock_calls == [ call('-s all'), call('-a sshuttle6-1024 -f /dev/stdin', b'table {2404:6800:4004:80c::33}\n' b'rdr pass on lo0 inet6 proto tcp from ! ::1 to ' b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n' b'rdr pass on lo0 inet6 proto udp ' b'to port 53 -> ::1 port 1026\n' b'pass out route-to lo0 inet6 proto tcp to ' b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n' b'pass out inet6 proto tcp to ' b'2404:6800:4004:80c::101f/128 port 8080:8080\n' b'pass out route-to lo0 inet6 proto udp ' b'to port 53 keep state\n'), call('-e'), ] assert call(['kldload', 'pf'], env=get_env()) in \ mock_subprocess_call.mock_calls mock_pf_get_dev.reset_mock() mock_ioctl.reset_mock() mock_pfctl.reset_mock() with pytest.raises(Exception) as excinfo: method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None) assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [] method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY), call(mock_pf_get_dev(), 0xCBE0441A, ANY), ] assert mock_pfctl.mock_calls == [ call('-s all'), call('-a sshuttle-1025 -f /dev/stdin', b'table {1.2.3.33}\n' b'rdr pass on lo0 inet proto tcp from ! 127.0.0.1 ' b'to 1.2.3.0/24 -> 127.0.0.1 port 1025\n' b'rdr pass on lo0 inet proto udp ' b'to port 53 -> 127.0.0.1 port 1027\n' b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n' b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n' b'pass out route-to lo0 inet proto udp ' b'to port 53 keep state\n'), call('-e'), ] mock_pf_get_dev.reset_mock() mock_ioctl.reset_mock() mock_pfctl.reset_mock() method.restore_firewall(1025, AF_INET, False, None) method.restore_firewall(1024, AF_INET6, False, None) assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [ call('-a sshuttle-1025 -F all'), call('-a sshuttle6-1024 -F all'), call("-d"), ] mock_pf_get_dev.reset_mock() mock_pfctl.reset_mock() mock_ioctl.reset_mock() @patch('sshuttle.helpers.verbose', new=3) @patch('sshuttle.methods.pf.pf', OpenBsd()) @patch('sshuttle.methods.pf.pfctl') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): mock_pfctl.side_effect = pfctl method = get_method('pf') assert method.name == 'pf' method.setup_firewall( 1024, 1026, [(AF_INET6, u'2404:6800:4004:80c::33')], AF_INET6, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], False, None) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xcd60441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY), ] assert mock_pfctl.mock_calls == [ call('-s Interfaces -i lo -v'), call('-f /dev/stdin', b'match on lo\n'), call('-s all'), call('-a sshuttle6-1024 -f /dev/stdin', b'table {2404:6800:4004:80c::33}\n' b'pass in on lo0 inet6 proto tcp to 2404:6800:4004:80c::/64 ' b'port 8000:9000 divert-to ::1 port 1024\n' b'pass in on lo0 inet6 proto udp ' b'to port 53 rdr-to ::1 port 1026\n' b'pass out inet6 proto tcp to 2404:6800:4004:80c::/64 ' b'port 8000:9000 route-to lo0 keep state\n' b'pass out inet6 proto tcp to ' b'2404:6800:4004:80c::101f/128 port 8080:8080\n' b'pass out inet6 proto udp to ' b' port 53 route-to lo0 keep state\n'), call('-e'), ] mock_pf_get_dev.reset_mock() mock_ioctl.reset_mock() mock_pfctl.reset_mock() with pytest.raises(Exception) as excinfo: method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None) assert str(excinfo.value) == 'UDP not supported by pf method_name' assert mock_pf_get_dev.mock_calls == [] assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [] method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], False, None) assert mock_ioctl.mock_calls == [ call(mock_pf_get_dev(), 0xcd60441a, ANY), call(mock_pf_get_dev(), 0xcd60441a, ANY), ] assert mock_pfctl.mock_calls == [ call('-s Interfaces -i lo -v'), call('-f /dev/stdin', b'match on lo\n'), call('-s all'), call('-a sshuttle-1025 -f /dev/stdin', b'table {1.2.3.33}\n' b'pass in on lo0 inet proto tcp to 1.2.3.0/24 divert-to ' b'127.0.0.1 port 1025\n' b'pass in on lo0 inet proto udp to ' b' port 53 rdr-to 127.0.0.1 port 1027\n' b'pass out inet proto tcp to 1.2.3.0/24 route-to lo0 keep state\n' b'pass out inet proto tcp to 1.2.3.66/32 port 80:80\n' b'pass out inet proto udp to ' b' port 53 route-to lo0 keep state\n'), call('-e'), ] mock_pf_get_dev.reset_mock() mock_ioctl.reset_mock() mock_pfctl.reset_mock() method.restore_firewall(1025, AF_INET, False, None) method.restore_firewall(1024, AF_INET6, False, None) assert mock_ioctl.mock_calls == [] assert mock_pfctl.mock_calls == [ call('-a sshuttle-1025 -F all'), call('-a sshuttle6-1024 -F all'), call('-d'), ] mock_pf_get_dev.reset_mock() mock_pfctl.reset_mock() mock_ioctl.reset_mock() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576185399.0 sshuttle-1.0.5/tests/client/test_methods_tproxy.py0000600000175000017500000003132600000000000021457 0ustar00brianbrianimport socket from socket import AF_INET, AF_INET6 from mock import Mock, patch, call from sshuttle.methods import get_method @patch("sshuttle.methods.tproxy.recvmsg") def test_get_supported_features_recvmsg(mock_recvmsg): method = get_method('tproxy') features = method.get_supported_features() assert features.ipv6 assert features.udp assert features.dns @patch("sshuttle.methods.tproxy.recvmsg", None) def test_get_supported_features_norecvmsg(): method = get_method('tproxy') features = method.get_supported_features() assert features.ipv6 assert not features.udp assert not features.dns def test_get_tcp_dstip(): sock = Mock() sock.getsockname.return_value = ('127.0.0.1', 1024) method = get_method('tproxy') assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024) assert sock.mock_calls == [call.getsockname()] @patch("sshuttle.methods.tproxy.recv_udp") def test_recv_udp(mock_recv_udp): mock_recv_udp.return_value = ("127.0.0.1", "127.0.0.2", "11111") sock = Mock() method = get_method('tproxy') result = method.recv_udp(sock, 1024) assert sock.mock_calls == [] assert mock_recv_udp.mock_calls == [call(sock, 1024)] assert result == ("127.0.0.1", "127.0.0.2", "11111") @patch("sshuttle.methods.socket.socket") def test_send_udp(mock_socket): sock = Mock() method = get_method('tproxy') method.send_udp(sock, "127.0.0.2", "127.0.0.1", "2222222") assert sock.mock_calls == [] assert mock_socket.mock_calls == [ call(sock.family, 2), call().setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), call().setsockopt(0, 19, 1), call().bind('127.0.0.2'), call().sendto("2222222", '127.0.0.1'), call().close() ] def test_setup_tcp_listener(): listener = Mock() method = get_method('tproxy') method.setup_tcp_listener(listener) assert listener.mock_calls == [ call.setsockopt(0, 19, 1) ] def test_setup_udp_listener(): listener = Mock() method = get_method('tproxy') method.setup_udp_listener(listener) assert listener.mock_calls == [ call.setsockopt(0, 19, 1), call.v4.setsockopt(0, 20, 1), call.v6.setsockopt(41, 74, 1) ] def test_assert_features(): method = get_method('tproxy') features = method.get_supported_features() method.assert_features(features) def test_firewall_command(): method = get_method('tproxy') assert not method.firewall_command("somthing") @patch('sshuttle.methods.tproxy.ipt') @patch('sshuttle.methods.tproxy.ipt_ttl') @patch('sshuttle.methods.tproxy.ipt_chain_exists') def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): mock_ipt_chain_exists.return_value = True method = get_method('tproxy') assert method.name == 'tproxy' # IPV6 method.setup_firewall( 1024, 1026, [(AF_INET6, u'2404:6800:4004:80c::33')], AF_INET6, [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], True, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'mangle', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', 'sshuttle-d-1024') ] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [ call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-N', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-N', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'), call(AF_INET6, 'mangle', '-N', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'), call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1024'), call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK', '--set-mark', '1'), call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', '-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', '-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32', '-m', 'udp', '-p', 'udp', '--dport', '53'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::33/32', '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', '--dest', u'2404:6800:4004:80c::101f/128', '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', '--dest', u'2404:6800:4004:80c::101f/128', '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', '--dest', u'2404:6800:4004:80c::101f/128', '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', '--dest', u'2404:6800:4004:80c::101f/128', '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000', '--on-port', '1024'), call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '-m', 'udp', '-p', 'udp', '--dport', '8000:9000'), call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '-m', 'udp', '-p', 'udp', '--dport', '8000:9000', '--on-port', '1024') ] mock_ipt_chain_exists.reset_mock() mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() method.restore_firewall(1025, AF_INET6, True, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET6, 'mangle', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', 'sshuttle-d-1025') ] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [ call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1025'), call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1025'), call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025') ] mock_ipt_chain_exists.reset_mock() mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() # IPV4 method.setup_firewall( 1025, 1027, [(AF_INET, u'1.2.3.33')], AF_INET, [(AF_INET, 24, False, u'1.2.3.0', 0, 0), (AF_INET, 32, True, u'1.2.3.66', 80, 80)], True, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'), call(AF_INET, 'mangle', 'sshuttle-d-1025') ] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [ call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-N', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-N', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-N', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'MARK', '--set-mark', '1'), call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', '-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', '-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', '--set-mark', '1', '--dest', u'1.2.3.33/32', '-m', 'udp', '-p', 'udp', '--dport', '53'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32', '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp', '--dport', '80:80'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp', '--dport', '80:80'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp', '--dport', '80:80'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp', '--dport', '80:80'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', '--set-mark', '1', '--dest', u'1.2.3.0/24', '-m', 'tcp', '-p', 'tcp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', '-m', 'tcp', '-p', 'tcp', '--on-port', '1025'), call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', '--set-mark', '1', '--dest', u'1.2.3.0/24', '-m', 'udp', '-p', 'udp'), call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', '-m', 'udp', '-p', 'udp', '--on-port', '1025') ] mock_ipt_chain_exists.reset_mock() mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() method.restore_firewall(1025, AF_INET, True, None) assert mock_ipt_chain_exists.mock_calls == [ call(AF_INET, 'mangle', 'sshuttle-m-1025'), call(AF_INET, 'mangle', 'sshuttle-t-1025'), call(AF_INET, 'mangle', 'sshuttle-d-1025') ] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [ call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'), call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'), call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'), call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025') ] mock_ipt_chain_exists.reset_mock() mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608109737.0 sshuttle-1.0.5/tests/client/test_options.py0000600000175000017500000000726700000000000020071 0ustar00brianbrianimport socket from argparse import ArgumentTypeError as Fatal import pytest import sshuttle.options _ip4_reprs = { '0.0.0.0': '0.0.0.0', '255.255.255.255': '255.255.255.255', '10.0': '10.0.0.0', '184.172.10.74': '184.172.10.74', '3098282570': '184.172.10.74', '0xb8.0xac.0x0a.0x4a': '184.172.10.74', '0270.0254.0012.0112': '184.172.10.74', } _ip4_swidths = (1, 8, 22, 27, 32) _ip6_reprs = { '::': '::', '::1': '::1', 'fc00::': 'fc00::', '2a01:7e00:e000:188::1': '2a01:7e00:e000:188::1' } _ip6_swidths = (48, 64, 96, 115, 128) def test_parse_subnetport_ip4(): for ip_repr, ip in _ip4_reprs.items(): assert sshuttle.options.parse_subnetport(ip_repr) \ == [(socket.AF_INET, ip, 32, 0, 0)] with pytest.raises(Fatal) as excinfo: sshuttle.options.parse_subnetport('10.256.0.0') assert str(excinfo.value) == 'Unable to resolve address: 10.256.0.0' def test_parse_subnetport_ip4_with_mask(): for ip_repr, ip in _ip4_reprs.items(): for swidth in _ip4_swidths: assert sshuttle.options.parse_subnetport( '/'.join((ip_repr, str(swidth))) ) == [(socket.AF_INET, ip, swidth, 0, 0)] assert sshuttle.options.parse_subnetport('0/0') \ == [(socket.AF_INET, '0.0.0.0', 0, 0, 0)] with pytest.raises(Fatal) as excinfo: sshuttle.options.parse_subnetport('10.0.0.0/33') assert str(excinfo.value) \ == 'Slash in CIDR notation (/33) is not between 0 and 32' def test_parse_subnetport_ip4_with_port(): for ip_repr, ip in _ip4_reprs.items(): assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \ == [(socket.AF_INET, ip, 32, 80, 80)] assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\ == [(socket.AF_INET, ip, 32, 80, 90)] def test_parse_subnetport_ip4_with_mask_and_port(): for ip_repr, ip in _ip4_reprs.items(): assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \ == [(socket.AF_INET, ip, 32, 80, 80)] assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \ == [(socket.AF_INET, ip, 16, 80, 90)] def test_parse_subnetport_ip6(): for ip_repr, ip in _ip6_reprs.items(): assert sshuttle.options.parse_subnetport(ip_repr) \ == [(socket.AF_INET6, ip, 128, 0, 0)] def test_parse_subnetport_ip6_with_mask(): for ip_repr, ip in _ip6_reprs.items(): for swidth in _ip4_swidths + _ip6_swidths: assert sshuttle.options.parse_subnetport( '/'.join((ip_repr, str(swidth))) ) == [(socket.AF_INET6, ip, swidth, 0, 0)] assert sshuttle.options.parse_subnetport('::/0') \ == [(socket.AF_INET6, '::', 0, 0, 0)] with pytest.raises(Fatal) as excinfo: sshuttle.options.parse_subnetport('fc00::/129') assert str(excinfo.value) \ == 'Slash in CIDR notation (/129) is not between 0 and 128' def test_parse_subnetport_ip6_with_port(): for ip_repr, ip in _ip6_reprs.items(): assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \ == [(socket.AF_INET6, ip, 128, 80, 80)] assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \ == [(socket.AF_INET6, ip, 128, 80, 90)] def test_parse_subnetport_ip6_with_mask_and_port(): for ip_repr, ip in _ip6_reprs.items(): assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \ == [(socket.AF_INET6, ip, 128, 80, 80)] assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\ == [(socket.AF_INET6, ip, 16, 80, 90)] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576185399.0 sshuttle-1.0.5/tests/client/test_sdnotify.py0000600000175000017500000000370100000000000020222 0ustar00brianbrianimport socket from mock import Mock, patch, call import sshuttle.sdnotify @patch('sshuttle.sdnotify.os.environ.get') def test_notify_invalid_socket_path(mock_get): mock_get.return_value = 'invalid_path' assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready()) @patch('sshuttle.sdnotify.os.environ.get') def test_notify_socket_not_there(mock_get): mock_get.return_value = '/run/valid_nonexistent_path' assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready()) @patch('sshuttle.sdnotify.os.environ.get') def test_notify_no_message(mock_get): mock_get.return_value = '/run/valid_path' assert not sshuttle.sdnotify.send() @patch('sshuttle.sdnotify.socket.socket') @patch('sshuttle.sdnotify.os.environ.get') def test_notify_socket_error(mock_get, mock_socket): mock_get.return_value = '/run/valid_path' mock_socket.side_effect = socket.error('test error') assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready()) @patch('sshuttle.sdnotify.socket.socket') @patch('sshuttle.sdnotify.os.environ.get') def test_notify_sendto_error(mock_get, mock_socket): message = sshuttle.sdnotify.ready() socket_path = '/run/valid_path' sock = Mock() sock.sendto.side_effect = socket.error('test error') mock_get.return_value = '/run/valid_path' mock_socket.return_value = sock assert not sshuttle.sdnotify.send(message) assert sock.sendto.mock_calls == [ call(message, socket_path), ] @patch('sshuttle.sdnotify.socket.socket') @patch('sshuttle.sdnotify.os.environ.get') def test_notify(mock_get, mock_socket): messages = [sshuttle.sdnotify.ready(), sshuttle.sdnotify.status('Running')] socket_path = '/run/valid_path' sock = Mock() sock.sendto.return_value = 1 mock_get.return_value = '/run/valid_path' mock_socket.return_value = sock assert sshuttle.sdnotify.send(*messages) assert sock.sendto.mock_calls == [ call(b'\n'.join(messages), socket_path), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/tests/server/0000700000175000017500000000000000000000000014777 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576185399.0 sshuttle-1.0.5/tests/server/test_server.py0000600000175000017500000000343500000000000017725 0ustar00brianbrianimport io import socket from mock import patch, Mock import sshuttle.server def test__ipmatch(): assert sshuttle.server._ipmatch("1.2.3.4") is not None assert sshuttle.server._ipmatch("::1") is None # ipv6 not supported assert sshuttle.server._ipmatch("42 Example Street, Melbourne") is None def test__ipstr(): assert sshuttle.server._ipstr("1.2.3.4", 24) == "1.2.3.4/24" assert sshuttle.server._ipstr("1.2.3.4", 32) == "1.2.3.4" def test__maskbits(): netmask = sshuttle.server._ipmatch("255.255.255.0") sshuttle.server._maskbits(netmask) @patch('sshuttle.server.which', side_effect=lambda x: x == 'netstat') @patch('sshuttle.server.ssubprocess.Popen') def test_listroutes_netstat(mock_popen, mock_which): mock_pobj = Mock() mock_pobj.stdout = io.BytesIO(b""" Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 wlan0 192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 wlan0 """) mock_pobj.wait.return_value = 0 mock_popen.return_value = mock_pobj routes = sshuttle.server.list_routes() assert list(routes) == [ (socket.AF_INET, '192.168.1.0', 24) ] @patch('sshuttle.server.which', side_effect=lambda x: x == 'ip') @patch('sshuttle.server.ssubprocess.Popen') def test_listroutes_iproute(mock_popen, mock_which): mock_pobj = Mock() mock_pobj.stdout = io.BytesIO(b""" default via 192.168.1.1 dev wlan0 proto static 192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.1 """) mock_pobj.wait.return_value = 0 mock_popen.return_value = mock_pobj routes = sshuttle.server.list_routes() assert list(routes) == [ (socket.AF_INET, '192.168.1.0', 24) ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1609198743.9825907 sshuttle-1.0.5/tests/ssh/0000700000175000017500000000000000000000000014266 5ustar00brianbrian././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597269693.0 sshuttle-1.0.5/tests/ssh/test_parse_hostport.py0000600000175000017500000000147600000000000020765 0ustar00brianbrianfrom sshuttle.ssh import parse_hostport def test_host_only(): assert parse_hostport("host") == (None, None, None, "host") assert parse_hostport("1.2.3.4") == (None, None, None, "1.2.3.4") assert parse_hostport("2001::1") == (None, None, None, "2001::1") assert parse_hostport("[2001::1]") == (None, None, None, "2001::1") def test_host_and_port(): assert parse_hostport("host:22") == (None, None, 22, "host") assert parse_hostport("1.2.3.4:22") == (None, None, 22, "1.2.3.4") assert parse_hostport("[2001::1]:22") == (None, None, 22, "2001::1") def test_username_and_host(): assert parse_hostport("user@host") == ("user", None, None, "host") assert parse_hostport("user:@host") == ("user", None, None, "host") assert parse_hostport("user:pass@host") == ("user", "pass", None, "host") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1609113643.0 sshuttle-1.0.5/tox.ini0000600000175000017500000000065300000000000013650 0ustar00brianbrian[tox] downloadcache = {toxworkdir}/cache/ envlist = py35, py36, py37, py38, py39, [testenv] basepython = py36: python3.6 py37: python3.7 py38: python3.8 py39: python3.9 commands = pip install -e . # actual flake8 test flake8 sshuttle tests # flake8 complexity warnings flake8 sshuttle tests --exit-zero --max-complexity=10 pytest deps = -rrequirements-tests.txt