├── .gitignore ├── .prospector.yml ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bandit.yml ├── conftest.py ├── docs ├── Makefile ├── changes.rst ├── conf.py ├── how-it-works.rst ├── index.rst ├── installation.rst ├── make.bat ├── manpage.rst ├── overview.rst ├── platform.rst ├── requirements.rst ├── support.rst ├── tproxy.rst ├── trivia.rst ├── usage.rst └── windows.rst ├── requirements-tests.txt ├── requirements.txt ├── run ├── setup.cfg ├── setup.py ├── sshuttle ├── __init__.py ├── __main__.py ├── assembler.py ├── client.py ├── cmdline.py ├── firewall.py ├── helpers.py ├── hostwatch.py ├── linux.py ├── methods │ ├── __init__.py │ ├── ipfw.py │ ├── nat.py │ ├── nft.py │ ├── pf.py │ └── tproxy.py ├── options.py ├── sdnotify.py ├── server.py ├── ssh.py ├── ssnet.py ├── ssyslog.py ├── stresstest.py └── tests │ ├── client │ ├── test_firewall.py │ ├── test_helpers.py │ ├── test_methods_nat.py │ ├── test_methods_pf.py │ ├── test_methods_tproxy.py │ ├── test_options.py │ └── test_sdnotify.py │ └── server │ └── test_server.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | /sshuttle/version.py 2 | /tmp/ 3 | /.cache/ 4 | /.eggs/ 5 | /.tox/ 6 | /build/ 7 | /dist/ 8 | /sshuttle.egg-info/ 9 | /docs/_build/ 10 | *.pyc 11 | *~ 12 | *.8 13 | /.do_built 14 | /.do_built.dir 15 | /.redo 16 | /.pytest_cache/ 17 | /.python-version 18 | -------------------------------------------------------------------------------- /.prospector.yml: -------------------------------------------------------------------------------- 1 | strictness: medium 2 | 3 | pylint: 4 | disable: 5 | - too-many-statements 6 | - too-many-locals 7 | - too-many-function-args 8 | - too-many-arguments 9 | - too-many-branches 10 | - bare-except 11 | - protected-access 12 | - no-else-return 13 | - unused-argument 14 | - method-hidden 15 | - arguments-differ 16 | - wrong-import-position 17 | - raising-bad-type 18 | 19 | pep8: 20 | options: 21 | max-line-length: 79 22 | 23 | mccabe: 24 | run: false 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | - 3.5 6 | - 3.6 7 | - pypy 8 | 9 | install: 10 | - travis_retry pip install -q -r requirements-tests.txt 11 | 12 | before_script: 13 | # stop the build if there are Python syntax errors or undefined names. 14 | - flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics 15 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide. 16 | - flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 17 | 18 | script: 19 | - PYTHONPATH=. py.test 20 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Change log 3 | ========== 4 | All notable changes to this project will be documented in this file. The format 5 | is based on `Keep a Changelog`_ and this project 6 | adheres to `Semantic Versioning`_. 7 | 8 | .. _`Keep a Changelog`: http://keepachangelog.com/ 9 | .. _`Semantic Versioning`: http://semver.org/ 10 | 11 | 12 | 0.78.4 - 2018-04-02 13 | ------------------- 14 | 15 | Added 16 | ~~~~~ 17 | * Add homebrew instructions. 18 | * Route traffic by linux user. 19 | * Add nat-like method using nftables instead of iptables. 20 | 21 | Changed 22 | ~~~~~~~ 23 | * Talk to custom DNS server on pod, instead of the ones in /etc/resolv.conf. 24 | * Add new option for overriding destination DNS server. 25 | * Changed subnet parsing. Previously 10/8 become 10.0.0.0/8. Now it gets 26 | parsed as 0.0.0.10/8. 27 | * Make hostwatch find both fqdn and hostname. 28 | * Use versions of python3 greater than 3.5 when available (e.g. 3.6). 29 | 30 | Removed 31 | ~~~~~~~ 32 | * Remove Python 2.6 from automatic tests. 33 | 34 | Fixed 35 | ~~~~~ 36 | * Fix case where there is no --dns. 37 | * [pf] Avoid port forwarding from loopback address. 38 | * Use getaddrinfo to obtain a correct sockaddr. 39 | * Skip empty lines on incoming routes data. 40 | * Just skip empty lines of routes data instead of stopping processing. 41 | * [pf] Load pf kernel module when enabling pf. 42 | * [pf] Test double restore (ipv4, ipv6) disables only once; test kldload. 43 | * Fixes UDP and DNS proxies binding to the same socket address. 44 | * Mock socket bind to avoid depending on local IPs being available in test box. 45 | * Fix no value passed for argument auto_hosts in hw_main call. 46 | * Fixed incorrect license information in setup.py. 47 | * Preserve peer and port properly. 48 | * Make --to-dns and --ns-host work well together. 49 | * Remove test that fails under OSX. 50 | * Specify pip requirements for tests. 51 | * Use flake8 to find Python syntax errors or undefined names. 52 | * Fix compatibility with the sudoers file. 53 | * Stop using SO_REUSEADDR on sockets. 54 | * Declare 'verbosity' as global variable to placate linters. 55 | * Adds 'cd sshuttle' after 'git' to README and docs. 56 | * Documentation for loading options from configuration file. 57 | * Load options from a file. 58 | * Fix firewall.py. 59 | * Move sdnotify after setting up firewall rules. 60 | * Fix tests on Macos. 61 | 62 | 63 | 0.78.3 - 2017-07-09 64 | ------------------- 65 | The "I should have done a git pull" first release. 66 | 67 | Fixed 68 | ~~~~~ 69 | * Order first by port range and only then by swidth 70 | 71 | 72 | 0.78.2 - 2017-07-09 73 | ------------------- 74 | 75 | Added 76 | ~~~~~ 77 | * Adds support for tunneling specific port ranges (#144). 78 | * Add support for iproute2. 79 | * Allow remote hosts with colons in the username. 80 | * Re-introduce ipfw support for sshuttle on FreeBSD with support for --DNS option as well. 81 | * Add support for PfSense. 82 | * Tests and documentation for systemd integration. 83 | * Allow subnets to be given only by file (-s). 84 | 85 | Fixed 86 | ~~~~~ 87 | * Work around non tabular headers in BSD netstat. 88 | * Fix UDP and DNS support on Python 2.7 with tproxy method. 89 | * Fixed tests after adding support for iproute2. 90 | * Small refactoring of netstat/iproute parsing. 91 | * Set started_by_sshuttle False after disabling pf. 92 | * Fix punctuation and explain Type=notify. 93 | * Move pytest-runner to tests_require. 94 | * Fix warning: closed channel got=STOP_SENDING. 95 | * Support sdnotify for better systemd integration. 96 | * Fix #117 to allow for no subnets via file (-s). 97 | * Fix argument splitting for multi-word arguments. 98 | * requirements.rst: Fix mistakes. 99 | * Fix typo, space not required here. 100 | * Update installation instructions. 101 | * Support using run from different directory. 102 | * Ensure we update sshuttle/version.py in run. 103 | * Don't print python version in run. 104 | * Add CWD to PYTHONPATH in run. 105 | 106 | 107 | 0.78.1 - 2016-08-06 108 | ------------------- 109 | * Fix readthedocs versioning. 110 | * Don't crash on ENETUNREACH. 111 | * Various bug fixes. 112 | * Improvements to BSD and OSX support. 113 | 114 | 115 | 0.78.0 - 2016-04-08 116 | ------------------- 117 | 118 | * Don't force IPv6 if IPv6 nameservers supplied. Fixes #74. 119 | * Call /bin/sh as users shell may not be POSIX compliant. Fixes #77. 120 | * Use argparse for command line processing. Fixes #75. 121 | * Remove useless --server option. 122 | * Support multiple -s (subnet) options. Fixes #86. 123 | * Make server parts work with old versions of Python. Fixes #81. 124 | 125 | 126 | 0.77.2 - 2016-03-07 127 | ------------------- 128 | 129 | * Accidentally switched LGPL2 license with GPL2 license in 0.77.1 - now fixed. 130 | 131 | 132 | 0.77.1 - 2016-03-07 133 | ------------------- 134 | 135 | * Use semantic versioning. http://semver.org/ 136 | * Update GPL 2 license text. 137 | * New release to fix PyPI. 138 | 139 | 140 | 0.77 - 2016-03-03 141 | ----------------- 142 | 143 | * Various bug fixes. 144 | * Fix Documentation. 145 | * Add fix for MacOS X issue. 146 | * Add support for OpenBSD. 147 | 148 | 149 | 0.76 - 2016-01-17 150 | ----------------- 151 | 152 | * Add option to disable IPv6 support. 153 | * Update documentation. 154 | * Move documentation, including man page, to Sphinx. 155 | * Use setuptools-scm for automatic versioning. 156 | 157 | 158 | 0.75 - 2016-01-12 159 | ----------------- 160 | 161 | * Revert change that broke sshuttle entry point. 162 | 163 | 164 | 0.74 - 2016-01-10 165 | ----------------- 166 | 167 | * Add CHANGES.rst file. 168 | * Numerous bug fixes. 169 | * Python 3.5 fixes. 170 | * PF fixes, especially for BSD. 171 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | include *.py 4 | include MANIFEST.in 5 | include LICENSE 6 | include run 7 | include tox.ini 8 | exclude sshuttle/version.py 9 | recursive-include docs *.bat 10 | recursive-include docs *.py 11 | recursive-include docs *.rst 12 | recursive-include docs Makefile 13 | recursive-include sshuttle *.py 14 | recursive-exclude docs/_build * 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sshuttle: where transparent proxy meets VPN meets ssh 2 | ===================================================== 3 | 4 | As far as I know, sshuttle is the only program that solves the following 5 | common case: 6 | 7 | - Your client machine (or router) is Linux, FreeBSD, or MacOS. 8 | 9 | - You have access to a remote network via ssh. 10 | 11 | - You don't necessarily have admin access on the remote network. 12 | 13 | - The remote network has no VPN, or only stupid/complex VPN 14 | protocols (IPsec, PPTP, etc). Or maybe you *are* the 15 | admin and you just got frustrated with the awful state of 16 | VPN tools. 17 | 18 | - You don't want to create an ssh port forward for every 19 | single host/port on the remote network. 20 | 21 | - You hate openssh's port forwarding because it's randomly 22 | slow and/or stupid. 23 | 24 | - You can't use openssh's PermitTunnel feature because 25 | it's disabled by default on openssh servers; plus it does 26 | TCP-over-TCP, which has terrible performance (see below). 27 | 28 | 29 | Obtaining sshuttle 30 | ------------------ 31 | 32 | - Debian stretch or later:: 33 | 34 | apt-get install sshuttle 35 | 36 | - From PyPI:: 37 | 38 | sudo pip install sshuttle 39 | 40 | - Clone:: 41 | 42 | git clone https://github.com/sshuttle/sshuttle.git 43 | cd sshuttle 44 | sudo ./setup.py install 45 | 46 | It is also possible to install into a virtualenv as a non-root user. 47 | 48 | - From PyPI:: 49 | 50 | virtualenv -p python3 /tmp/sshuttle 51 | . /tmp/sshuttle/bin/activate 52 | pip install sshuttle 53 | 54 | - Clone:: 55 | 56 | virtualenv -p python3 /tmp/sshuttle 57 | . /tmp/sshuttle/bin/activate 58 | git clone https://github.com/sshuttle/sshuttle.git 59 | cd sshuttle 60 | ./setup.py install 61 | 62 | - Homebrew:: 63 | 64 | brew install sshuttle 65 | 66 | 67 | Documentation 68 | ------------- 69 | The documentation for the stable version is available at: 70 | http://sshuttle.readthedocs.org/ 71 | 72 | The documentation for the latest development version is available at: 73 | http://sshuttle.readthedocs.org/en/latest/ 74 | -------------------------------------------------------------------------------- /bandit.yml: -------------------------------------------------------------------------------- 1 | exclude_dirs: 2 | - sshuttle/tests 3 | skips: 4 | - B101 5 | - B104 6 | - B404 7 | - B603 8 | - B606 9 | - B607 10 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 0): 4 | good_python = sys.version_info >= (3, 5) 5 | else: 6 | good_python = sys.version_info >= (2, 7) 7 | 8 | collect_ignore = [] 9 | if not good_python: 10 | collect_ignore.append("sshuttle/tests/client") 11 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sshuttle.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sshuttle.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/sshuttle" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sshuttle" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # sshuttle documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Jan 17 12:13:47 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | sys.path.insert(0, os.path.abspath('..')) 19 | import sshuttle.version # NOQA 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | # sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.todo', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | # source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = 'sshuttle' 52 | copyright = '2016, Brian May' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The full version, including alpha/beta/rc tags. 59 | release = sshuttle.version.version 60 | # The short X.Y version. 61 | version = '.'.join(release.split('.')[:2]) 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # language = None 66 | 67 | # There are two options for replacing |today|: either, you set today to some 68 | # non-false value, then it is used: 69 | # today = '' 70 | # Else, today_fmt is used as the format for a strftime call. 71 | # today_fmt = '%B %d, %Y' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = ['_build'] 76 | 77 | # The reST default role (used for this markup: `text`) to use for all 78 | # documents. 79 | # default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | # add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | # add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | # show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = 'sphinx' 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | # modindex_common_prefix = [] 97 | 98 | # If true, keep warnings as "system message" paragraphs in the built documents. 99 | # keep_warnings = False 100 | 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | html_theme = 'default' 107 | 108 | # Theme options are theme-specific and customize the look and feel of a theme 109 | # further. For a list of options available for each theme, see the 110 | # documentation. 111 | # html_theme_options = {} 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | # html_theme_path = [] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | # html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | # html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | # html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | # html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = ['_static'] 136 | 137 | # Add any extra paths that contain custom files (such as robots.txt or 138 | # .htaccess) here, relative to this directory. These files are copied 139 | # directly to the root of the documentation. 140 | # html_extra_path = [] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | # html_last_updated_fmt = '%b %d, %Y' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | # html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | # html_sidebars = {} 152 | 153 | # Additional templates that should be rendered to pages, maps page names to 154 | # template names. 155 | # html_additional_pages = {} 156 | 157 | # If false, no module index is generated. 158 | # html_domain_indices = True 159 | 160 | # If false, no index is generated. 161 | # html_use_index = True 162 | 163 | # If true, the index is split into individual pages for each letter. 164 | # html_split_index = False 165 | 166 | # If true, links to the reST sources are added to the pages. 167 | # html_show_sourcelink = True 168 | 169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 170 | # html_show_sphinx = True 171 | 172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 173 | # html_show_copyright = True 174 | 175 | # If true, an OpenSearch description file will be output, and all pages will 176 | # contain a tag referring to it. The value of this option must be the 177 | # base URL from which the finished HTML is served. 178 | # html_use_opensearch = '' 179 | 180 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 181 | # html_file_suffix = None 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = 'sshuttledoc' 185 | 186 | 187 | # -- Options for LaTeX output --------------------------------------------- 188 | 189 | latex_elements = { 190 | # The paper size ('letterpaper' or 'a4paper'). 191 | # 'papersize': 'letterpaper', 192 | 193 | # The font size ('10pt', '11pt' or '12pt'). 194 | # 'pointsize': '10pt', 195 | 196 | # Additional stuff for the LaTeX preamble. 197 | # 'preamble': '', 198 | } 199 | 200 | # Grouping the document tree into LaTeX files. List of tuples 201 | # (source start file, target name, title, 202 | # author, documentclass [howto, manual, or own class]). 203 | latex_documents = [ 204 | ('index', 'sshuttle.tex', 'sshuttle documentation', 'Brian May', 'manual'), 205 | ] 206 | 207 | # The name of an image file (relative to this directory) to place at the top of 208 | # the title page. 209 | # latex_logo = None 210 | 211 | # For "manual" documents, if this is true, then toplevel headings are parts, 212 | # not chapters. 213 | # latex_use_parts = False 214 | 215 | # If true, show page references after internal links. 216 | # latex_show_pagerefs = False 217 | 218 | # If true, show URL addresses after external links. 219 | # latex_show_urls = False 220 | 221 | # Documents to append as an appendix to all manuals. 222 | # latex_appendices = [] 223 | 224 | # If false, no module index is generated. 225 | # latex_domain_indices = True 226 | 227 | 228 | # -- Options for manual page output --------------------------------------- 229 | 230 | # One entry per manual page. List of tuples 231 | # (source start file, name, description, authors, manual section). 232 | man_pages = [ 233 | ('manpage', 'sshuttle', 'sshuttle documentation', ['Brian May'], 1) 234 | ] 235 | 236 | # If true, show URL addresses after external links. 237 | # man_show_urls = False 238 | 239 | 240 | # -- Options for Texinfo output ------------------------------------------- 241 | 242 | # Grouping the document tree into Texinfo files. List of tuples 243 | # (source start file, target name, title, author, 244 | # dir menu entry, description, category) 245 | texinfo_documents = [ 246 | ('index', 'sshuttle', 'sshuttle documentation', 247 | 'Brian May', 'sshuttle', 'A transparent proxy-based VPN using ssh', 248 | 'Miscellaneous'), 249 | ] 250 | 251 | # Documents to append as an appendix to all manuals. 252 | # texinfo_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | # texinfo_domain_indices = True 256 | 257 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 258 | # texinfo_show_urls = 'footnote' 259 | 260 | # If true, do not generate a @detailmenu in the "Top" node's menu. 261 | # texinfo_no_detailmenu = False 262 | -------------------------------------------------------------------------------- /docs/how-it-works.rst: -------------------------------------------------------------------------------- 1 | How it works 2 | ============ 3 | sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind 4 | of both, and kind of neither. 5 | 6 | It's like a VPN, since it can forward every port on an entire network, not 7 | just ports you specify. Conveniently, it lets you use the "real" IP 8 | addresses of each host rather than faking port numbers on localhost. 9 | 10 | On the other hand, the way it *works* is more like ssh port forwarding than 11 | a VPN. Normally, a VPN forwards your data one packet at a time, and 12 | doesn't care about individual connections; ie. it's "stateless" with respect 13 | to the traffic. sshuttle is the opposite of stateless; it tracks every 14 | single connection. 15 | 16 | You could compare sshuttle to something like the old `Slirp 17 | `_ program, which was a userspace TCP/IP 18 | implementation that did something similar. But it operated on a 19 | packet-by-packet basis on the client side, reassembling the packets on the 20 | server side. That worked okay back in the "real live serial port" days, 21 | because serial ports had predictable latency and buffering. 22 | 23 | But you can't safely just forward TCP packets over a TCP session (like ssh), 24 | because TCP's performance depends fundamentally on packet loss; it 25 | *must* experience packet loss in order to know when to slow down! At 26 | the same time, the outer TCP session (ssh, in this case) is a reliable 27 | transport, which means that what you forward through the tunnel *never* 28 | experiences packet loss. The ssh session itself experiences packet loss, of 29 | course, but TCP fixes it up and ssh (and thus you) never know the 30 | difference. But neither does your inner TCP session, and extremely screwy 31 | performance ensues. 32 | 33 | sshuttle assembles the TCP stream locally, multiplexes it statefully over 34 | an ssh session, and disassembles it back into packets at the other end. So 35 | it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is 36 | safe. 37 | 38 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | sshuttle: where transparent proxy meets VPN meets ssh 2 | ===================================================== 3 | 4 | :Date: |today| 5 | :Version: |version| 6 | 7 | Contents: 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | overview 13 | requirements 14 | installation 15 | usage 16 | platform 17 | Man Page 18 | how-it-works 19 | support 20 | trivia 21 | changes 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | - From PyPI:: 5 | 6 | pip install sshuttle 7 | 8 | - Clone:: 9 | 10 | git clone https://github.com/sshuttle/sshuttle.git 11 | cd sshuttle 12 | ./setup.py install 13 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sshuttle.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sshuttle.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/manpage.rst: -------------------------------------------------------------------------------- 1 | sshuttle 2 | ======== 3 | 4 | 5 | Synopsis 6 | -------- 7 | **sshuttle** [*options*] [**-r** *[username@]sshserver[:port]*] \<*subnets* ...\> 8 | 9 | 10 | Description 11 | ----------- 12 | :program:`sshuttle` allows you to create a VPN connection from your 13 | machine to any remote server that you can connect to via 14 | ssh, as long as that server has python 2.3 or higher. 15 | 16 | To work, you must have root access on the local machine, 17 | but you can have a normal account on the server. 18 | 19 | It's valid to run :program:`sshuttle` more than once simultaneously on 20 | a single client machine, connecting to a different server 21 | every time, so you can be on more than one VPN at once. 22 | 23 | If run on a router, :program:`sshuttle` can forward traffic for your 24 | entire subnet to the VPN. 25 | 26 | 27 | Options 28 | ------- 29 | .. program:: sshuttle 30 | 31 | .. option:: subnets 32 | 33 | A list of subnets to route over the VPN, in the form 34 | ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a 35 | single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4), 36 | 1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 37 | netmask), and 0/0 ('just route everything through the 38 | VPN'). Any of the previous examples are also valid if you append 39 | a port or a port range, so 1.2.3.4:8000 will only tunnel traffic 40 | that has as the destination port 8000 of 1.2.3.4 and 41 | 1.2.3.0/24:8000-9000 will tunnel traffic going to any port between 42 | 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet. 43 | It is also possible to use a name in which case the first IP it resolves 44 | to during startup will be routed over the VPN. Valid examples are 45 | example.com, example.com:8000 and example.com:8000-9000. 46 | 47 | .. option:: --method [auto|nat|tproxy|pf] 48 | 49 | Which firewall method should sshuttle use? For auto, sshuttle attempts to 50 | guess the appropriate method depending on what it can find in PATH. The 51 | default value is auto. 52 | 53 | .. option:: -l, --listen=[ip:]port 54 | 55 | Use this ip address and port number as the transparent 56 | proxy port. By default :program:`sshuttle` finds an available 57 | port automatically and listens on IP 127.0.0.1 58 | (localhost), so you don't need to override it, and 59 | connections are only proxied from the local machine, 60 | not from outside machines. If you want to accept 61 | connections from other machines on your network (ie. to 62 | run :program:`sshuttle` on a router) try enabling IP Forwarding in 63 | your kernel, then using ``--listen 0.0.0.0:0``. 64 | You can use any name resolving to an IP address of the machine running 65 | :program:`sshuttle`, e.g. ``--listen localhost``. 66 | 67 | For the tproxy and pf methods this can be an IPv6 address. Use this option 68 | twice if required, to provide both IPv4 and IPv6 addresses. 69 | 70 | .. option:: -H, --auto-hosts 71 | 72 | Scan for remote hostnames and update the local /etc/hosts 73 | file with matching entries for as long as the VPN is 74 | open. This is nicer than changing your system's DNS 75 | (/etc/resolv.conf) settings, for several reasons. First, 76 | hostnames are added without domain names attached, so 77 | you can ``ssh thatserver`` without worrying if your local 78 | domain matches the remote one. Second, if you :program:`sshuttle` 79 | into more than one VPN at a time, it's impossible to 80 | use more than one DNS server at once anyway, but 81 | :program:`sshuttle` correctly merges /etc/hosts entries between 82 | all running copies. Third, if you're only routing a 83 | few subnets over the VPN, you probably would prefer to 84 | keep using your local DNS server for everything else. 85 | 86 | .. option:: -N, --auto-nets 87 | 88 | In addition to the subnets provided on the command 89 | line, ask the server which subnets it thinks we should 90 | route, and route those automatically. The suggestions 91 | are taken automatically from the server's routing 92 | table. 93 | 94 | .. option:: --dns 95 | 96 | Capture local DNS requests and forward to the remote DNS 97 | server. 98 | 99 | .. option:: --python 100 | 101 | Specify the name/path of the remote python interpreter. 102 | The default is just ``python``, which means to use the 103 | default python interpreter on the remote system's PATH. 104 | 105 | .. option:: -r, --remote=[username@]sshserver[:port] 106 | 107 | The remote hostname and optional username and ssh 108 | port number to use for connecting to the remote server. 109 | For example, example.com, testuser@example.com, 110 | testuser@example.com:2222, or example.com:2244. 111 | 112 | .. option:: -x, --exclude=subnet 113 | 114 | Explicitly exclude this subnet from forwarding. The 115 | format of this option is the same as the ```` 116 | option. To exclude more than one subnet, specify the 117 | ``-x`` option more than once. You can say something like 118 | ``0/0 -x 1.2.3.0/24`` to forward everything except the 119 | local subnet over the VPN, for example. 120 | 121 | .. option:: -X, --exclude-from=file 122 | 123 | Exclude the subnets specified in a file, one subnet per 124 | line. Useful when you have lots of subnets to exclude. 125 | 126 | .. option:: -v, --verbose 127 | 128 | Print more information about the session. This option 129 | can be used more than once for increased verbosity. By 130 | default, :program:`sshuttle` prints only error messages. 131 | 132 | .. option:: -e, --ssh-cmd 133 | 134 | The command to use to connect to the remote server. The 135 | default is just ``ssh``. Use this if your ssh client is 136 | in a non-standard location or you want to provide extra 137 | options to the ssh command, for example, ``-e 'ssh -v'``. 138 | 139 | .. option:: --seed-hosts 140 | 141 | A comma-separated list of hostnames to use to 142 | initialize the :option:`--auto-hosts` scan algorithm. 143 | :option:`--auto-hosts` does things like poll local SMB servers 144 | for lists of local hostnames, but can speed things up 145 | if you use this option to give it a few names to start 146 | from. 147 | 148 | If this option is used *without* :option:`--auto-hosts`, 149 | then the listed hostnames will be scanned and added, but 150 | no further hostnames will be added. 151 | 152 | .. option:: --no-latency-control 153 | 154 | Sacrifice latency to improve bandwidth benchmarks. ssh 155 | uses really big socket buffers, which can overload the 156 | connection if you start doing large file transfers, 157 | thus making all your other sessions inside the same 158 | tunnel go slowly. Normally, :program:`sshuttle` tries to avoid 159 | this problem using a "fullness check" that allows only 160 | a certain amount of outstanding data to be buffered at 161 | a time. But on high-bandwidth links, this can leave a 162 | lot of your bandwidth underutilized. It also makes 163 | :program:`sshuttle` seem slow in bandwidth benchmarks (benchmarks 164 | rarely test ping latency, which is what :program:`sshuttle` is 165 | trying to control). This option disables the latency 166 | control feature, maximizing bandwidth usage. Use at 167 | your own risk. 168 | 169 | .. option:: -D, --daemon 170 | 171 | Automatically fork into the background after connecting 172 | to the remote server. Implies :option:`--syslog`. 173 | 174 | .. option:: --syslog 175 | 176 | after connecting, send all log messages to the 177 | :manpage:`syslog(3)` service instead of stderr. This is 178 | implicit if you use :option:`--daemon`. 179 | 180 | .. option:: --pidfile=pidfilename 181 | 182 | when using :option:`--daemon`, save :program:`sshuttle`'s pid to 183 | *pidfilename*. The default is ``sshuttle.pid`` in the 184 | current directory. 185 | 186 | .. option:: --disable-ipv6 187 | 188 | If using tproxy or pf methods, this will disable IPv6 support. 189 | 190 | .. option:: --firewall 191 | 192 | (internal use only) run the firewall manager. This is 193 | the only part of :program:`sshuttle` that must run as root. If 194 | you start :program:`sshuttle` as a non-root user, it will 195 | automatically run ``sudo`` or ``su`` to start the firewall 196 | manager, but the core of :program:`sshuttle` still runs as a 197 | normal user. 198 | 199 | .. option:: --hostwatch 200 | 201 | (internal use only) run the hostwatch daemon. This 202 | process runs on the server side and collects hostnames for 203 | the :option:`--auto-hosts` option. Using this option by itself 204 | makes it a lot easier to debug and test the :option:`--auto-hosts` 205 | feature. 206 | 207 | 208 | Configuration File 209 | ------------------ 210 | All the options described above can optionally be specified in a configuration 211 | file. 212 | 213 | To run :program:`sshuttle` with options defined in, e.g., `/etc/ssshuttle.conf` 214 | just pass the path to the file preceded by the `@` character, e.g. 215 | :option:`@/etc/ssshuttle.conf`. 216 | 217 | When running :program:`sshuttle` with options defined in a configuratio file, 218 | options can still be passed via the command line in addition to what is 219 | defined in the file. If a given option is defined both in the file and in 220 | the command line, the value in the command line will take precedence. 221 | 222 | Arguments read from a file must be one per line, as shown below:: 223 | 224 | value 225 | --option1 226 | value1 227 | --option2 228 | value2 229 | 230 | 231 | Examples 232 | -------- 233 | Test locally by proxying all local connections, without using ssh:: 234 | 235 | $ sshuttle -v 0/0 236 | 237 | Starting sshuttle proxy. 238 | Listening on ('0.0.0.0', 12300). 239 | [local sudo] Password: 240 | firewall manager ready. 241 | c : connecting to server... 242 | s: available routes: 243 | s: 192.168.42.0/24 244 | c : connected. 245 | firewall manager: starting transproxy. 246 | c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139. 247 | c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443. 248 | ...etc... 249 | ^C 250 | firewall manager: undoing changes. 251 | KeyboardInterrupt 252 | c : Keyboard interrupt: exiting. 253 | c : SW#8:192.168.42.121:47523: deleting 254 | c : SW#6:192.168.42.106:50035: deleting 255 | 256 | Test connection to a remote server, with automatic hostname 257 | and subnet guessing:: 258 | 259 | $ sshuttle -vNHr example.org 260 | 261 | Starting sshuttle proxy. 262 | Listening on ('0.0.0.0', 12300). 263 | firewall manager ready. 264 | c : connecting to server... 265 | s: available routes: 266 | s: 77.141.99.0/24 267 | c : connected. 268 | c : seed_hosts: [] 269 | firewall manager: starting transproxy. 270 | hostwatch: Found: testbox1: 1.2.3.4 271 | hostwatch: Found: mytest2: 5.6.7.8 272 | hostwatch: Found: domaincontroller: 99.1.2.3 273 | c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22. 274 | ^C 275 | firewall manager: undoing changes. 276 | c : Keyboard interrupt: exiting. 277 | c : SW#6:192.168.42.121:60554: deleting 278 | 279 | Run :program:`sshuttle` with a `/etc/sshuttle.conf` configuration file:: 280 | 281 | $ sshuttle @/etc/sshuttle.conf 282 | 283 | Use the options defined in `/etc/sshuttle.conf` but be more verbose:: 284 | 285 | $ sshuttle @/etc/sshuttle.conf -vvv 286 | 287 | Override the remote server defined in `/etc/sshuttle.conf`:: 288 | 289 | $ sshuttle @/etc/sshuttle.conf -r otheruser@test.example.com 290 | 291 | Example configuration file:: 292 | 293 | 192.168.0.0/16 294 | --remote 295 | user@example.com 296 | 297 | 298 | Discussion 299 | ---------- 300 | When it starts, :program:`sshuttle` creates an ssh session to the 301 | server specified by the ``-r`` option. If ``-r`` is omitted, 302 | it will start both its client and server locally, which is 303 | sometimes useful for testing. 304 | 305 | After connecting to the remote server, :program:`sshuttle` uploads its 306 | (python) source code to the remote end and executes it 307 | there. Thus, you don't need to install :program:`sshuttle` on the 308 | remote server, and there are never :program:`sshuttle` version 309 | conflicts between client and server. 310 | 311 | Unlike most VPNs, :program:`sshuttle` forwards sessions, not packets. 312 | That is, it uses kernel transparent proxying (`iptables 313 | REDIRECT` rules on Linux) to 314 | capture outgoing TCP sessions, then creates entirely 315 | separate TCP sessions out to the original destination at 316 | the other end of the tunnel. 317 | 318 | Packet-level forwarding (eg. using the tun/tap devices on 319 | Linux) seems elegant at first, but it results in 320 | several problems, notably the 'tcp over tcp' problem. The 321 | tcp protocol depends fundamentally on packets being dropped 322 | in order to implement its congestion control agorithm; if 323 | you pass tcp packets through a tcp-based tunnel (such as 324 | ssh), the inner tcp packets will never be dropped, and so 325 | the inner tcp stream's congestion control will be 326 | completely broken, and performance will be terrible. Thus, 327 | packet-based VPNs (such as IPsec and openvpn) cannot use 328 | tcp-based encrypted streams like ssh or ssl, and have to 329 | implement their own encryption from scratch, which is very 330 | complex and error prone. 331 | 332 | :program:`sshuttle`'s simplicity comes from the fact that it can 333 | safely use the existing ssh encrypted tunnel without 334 | incurring a performance penalty. It does this by letting 335 | the client-side kernel manage the incoming tcp stream, and 336 | the server-side kernel manage the outgoing tcp stream; 337 | there is no need for congestion control to be shared 338 | between the two separate streams, so a tcp-based tunnel is 339 | fine. 340 | 341 | .. seealso:: 342 | 343 | :manpage:`ssh(1)`, :manpage:`python(1)` 344 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | As far as I know, sshuttle is the only program that solves the following 5 | common case: 6 | 7 | - Your client machine (or router) is Linux, MacOS, FreeBSD, OpenBSD or pfSense. 8 | 9 | - You have access to a remote network via ssh. 10 | 11 | - You don't necessarily have admin access on the remote network. 12 | 13 | - The remote network has no VPN, or only stupid/complex VPN 14 | protocols (IPsec, PPTP, etc). Or maybe you *are* the 15 | admin and you just got frustrated with the awful state of 16 | VPN tools. 17 | 18 | - You don't want to create an ssh port forward for every 19 | single host/port on the remote network. 20 | 21 | - You hate openssh's port forwarding because it's randomly 22 | slow and/or stupid. 23 | 24 | - You can't use openssh's PermitTunnel feature because 25 | it's disabled by default on openssh servers; plus it does 26 | TCP-over-TCP, which has terrible performance (see below). 27 | -------------------------------------------------------------------------------- /docs/platform.rst: -------------------------------------------------------------------------------- 1 | Platform Specific Notes 2 | ======================= 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | tproxy 10 | windows 11 | -------------------------------------------------------------------------------- /docs/requirements.rst: -------------------------------------------------------------------------------- 1 | Requirements 2 | ============ 3 | 4 | Client side Requirements 5 | ------------------------ 6 | 7 | - sudo, or root access on your client machine. 8 | (The server doesn't need admin access.) 9 | - Python 2.7 or Python 3.5. 10 | 11 | 12 | Linux with NAT method 13 | ~~~~~~~~~~~~~~~~~~~~~ 14 | Supports: 15 | 16 | * IPv4 TCP 17 | * IPv4 DNS 18 | 19 | Requires: 20 | 21 | * iptables DNAT, REDIRECT, and ttl modules. 22 | 23 | 24 | Linux with TPROXY method 25 | ~~~~~~~~~~~~~~~~~~~~~~~~ 26 | Supports: 27 | 28 | * IPv4 TCP 29 | * IPv4 UDP (requires ``recvmsg`` - see below) 30 | * IPv6 DNS (requires ``recvmsg`` - see below) 31 | * IPv6 TCP 32 | * IPv6 UDP (requires ``recvmsg`` - see below) 33 | * IPv6 DNS (requires ``recvmsg`` - see below) 34 | 35 | .. _PyXAPI: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/ 36 | 37 | Full UDP or DNS support with the TPROXY method requires the ``recvmsg()`` 38 | syscall. This is not available in Python 2, however it is in Python 3.5 and 39 | later. Under Python 2 you might find it sufficient to install PyXAPI_ in 40 | order to get the ``recvmsg()`` function. See :doc:`tproxy` for more 41 | information. 42 | 43 | 44 | MacOS / FreeBSD / OpenBSD / pfSense 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | Method: pf 47 | 48 | Supports: 49 | 50 | * IPv4 TCP 51 | * IPv4 DNS 52 | * IPv6 TCP 53 | * IPv6 DNS 54 | 55 | Requires: 56 | 57 | * You need to have the pfctl command. 58 | 59 | Windows 60 | ~~~~~~~ 61 | 62 | Not officially supported, however can be made to work with Vagrant. Requires 63 | cmd.exe with Administrator access. See :doc:`windows` for more information. 64 | 65 | 66 | Server side Requirements 67 | ------------------------ 68 | The server can run in any version of Python between 2.4 and 3.6. 69 | However it is recommended that you use Python 2.7, Python 3.5 or later whenever 70 | possible as support for older versions might be dropped in the future. 71 | 72 | 73 | Additional Suggested Software 74 | ----------------------------- 75 | 76 | - You may want to use autossh, available in various package management 77 | systems. 78 | - If you are using systemd, sshuttle can notify it when the connection to 79 | the remote end is established and the firewall rules are installed. For 80 | this feature to work you must configure the process start-up type for the 81 | sshuttle service unit to notify, as shown in the example below. 82 | 83 | .. code-block:: ini 84 | :emphasize-lines: 6 85 | 86 | [Unit] 87 | Description=sshuttle 88 | After=network.target 89 | 90 | [Service] 91 | Type=notify 92 | ExecStart=/usr/bin/sshuttle --dns --remote @ 93 | 94 | [Install] 95 | WantedBy=multi-user.target 96 | -------------------------------------------------------------------------------- /docs/support.rst: -------------------------------------------------------------------------------- 1 | Support 2 | ======= 3 | 4 | Mailing list: 5 | 6 | * Subscribe by sending a message to 7 | * List archives are at: http://groups.google.com/group/sshuttle 8 | 9 | Issue tracker and pull requests at github: 10 | 11 | * https://github.com/sshuttle/sshuttle 12 | -------------------------------------------------------------------------------- /docs/tproxy.rst: -------------------------------------------------------------------------------- 1 | TPROXY 2 | ====== 3 | TPROXY is the only method that has full support of IPv6 and UDP. 4 | 5 | There are some things you need to consider for TPROXY to work: 6 | 7 | - The following commands need to be run first as root. This only needs to be 8 | done once after booting up:: 9 | 10 | ip route add local default dev lo table 100 11 | ip rule add fwmark 1 lookup 100 12 | ip -6 route add local default dev lo table 100 13 | ip -6 rule add fwmark 1 lookup 100 14 | 15 | - The ``--auto-nets`` feature does not detect IPv6 routes automatically. Add IPv6 16 | routes manually. e.g. by adding ``'::/0'`` to the end of the command line. 17 | 18 | - The client needs to be run as root. e.g.:: 19 | 20 | sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ... 21 | 22 | - You may need to exclude the IP address of the server you are connecting to. 23 | Otherwise sshuttle may attempt to intercept the ssh packets, which will not 24 | work. Use the ``--exclude`` parameter for this. 25 | 26 | - Similarly, UDP return packets (including DNS) could get intercepted and 27 | bounced back. This is the case if you have a broad subnet such as 28 | ``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the 29 | ``--exclude`` parameter for this. 30 | 31 | - You need the ``--method=tproxy`` parameter, as above. 32 | 33 | - The routes for the outgoing packets must already exist. For example, if your 34 | connection does not have IPv6 support, no IPv6 routes will exist, IPv6 35 | packets will not be generated and sshuttle cannot intercept them:: 36 | 37 | telnet -6 www.google.com 80 38 | Trying 2404:6800:4001:805::1010... 39 | telnet: Unable to connect to remote host: Network is unreachable 40 | 41 | Add some dummy routes to external interfaces. Make sure they get removed 42 | however after sshuttle exits. 43 | -------------------------------------------------------------------------------- /docs/trivia.rst: -------------------------------------------------------------------------------- 1 | Useless Trivia 2 | ============== 3 | This section written by the original author, Avery Pennarun 4 | . 5 | 6 | Back in 1998, I released the first version of `Tunnel 7 | Vision `_, a semi-intelligent VPN 8 | client for Linux. Unfortunately, I made two big mistakes: I implemented the 9 | key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops). 10 | The resulting program worked okay - and people used it for years - but the 11 | performance was always a bit funny. And nobody ever found any security flaws 12 | in my key exchange, either, but that doesn't mean anything. :) 13 | 14 | The same year, dcoombs and I also released Fast Forward, a proxy server 15 | supporting transparent proxying. Among other things, we used it for 16 | automatically splitting traffic across more than one Internet connection (a 17 | tool we called "Double Vision"). 18 | 19 | I was still in university at the time. A couple years after that, one of my 20 | professors was working with some graduate students on the technology that would 21 | eventually become `Slipstream Internet Acceleration 22 | `_. He asked me to do a contract for him to build 23 | an initial prototype of a transparent proxy server for mobile networks. The 24 | idea was similar to sshuttle: if you reassemble and then disassemble the TCP 25 | packets, you can reduce latency and improve performance vs. just forwarding 26 | the packets over a plain VPN or mobile network. (It's unlikely that any of my 27 | code has persisted in the Slipstream product today, but the concept is still 28 | pretty cool. I'm still horrified that people use plain TCP on complex mobile 29 | networks with crazily variable latency, for which it was never really 30 | intended.) 31 | 32 | That project I did for Slipstream was what first gave me the idea to merge 33 | the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single 34 | program that was the best of all worlds. And here we are, at last. 35 | You're welcome. 36 | 37 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. note:: 5 | 6 | For information on usage with Windows, see the :doc:`windows` section. 7 | For information on using the TProxy method, see the :doc:`tproxy` section. 8 | 9 | Forward all traffic:: 10 | 11 | sshuttle -r username@sshserver 0.0.0.0/0 12 | 13 | - Use the :option:`sshuttle -r` parameter to specify a remote server. 14 | 15 | - By default sshuttle will automatically choose a method to use. Override with 16 | the :option:`sshuttle --method` parameter. 17 | 18 | - There is a shortcut for 0.0.0.0/0 for those that value 19 | their wrists:: 20 | 21 | sshuttle -r username@sshserver 0/0 22 | 23 | If you would also like your DNS queries to be proxied 24 | through the DNS server of the server you are connect to:: 25 | 26 | sshuttle --dns -r username@sshserver 0/0 27 | 28 | The above is probably what you want to use to prevent 29 | local network attacks such as Firesheep and friends. 30 | See the documentation for the :option:`sshuttle --dns` parameter. 31 | 32 | (You may be prompted for one or more passwords; first, the local password to 33 | become root using sudo, and then the remote ssh password. Or you might have 34 | sudo and ssh set up to not require passwords, in which case you won't be 35 | prompted at all.) 36 | 37 | 38 | Usage Notes 39 | ----------- 40 | That's it! Now your local machine can access the remote network as if you 41 | were right there. And if your "client" machine is a router, everyone on 42 | your local network can make connections to your remote network. 43 | 44 | You don't need to install sshuttle on the remote server; 45 | the remote server just needs to have python available. 46 | sshuttle will automatically upload and run its source code 47 | to the remote python interpreter. 48 | 49 | This creates a transparent proxy server on your local machine for all IP 50 | addresses that match 0.0.0.0/0. (You can use more specific IP addresses if 51 | you want; use any number of IP addresses or subnets to change which 52 | addresses get proxied. Using 0.0.0.0/0 proxies *everything*, which is 53 | interesting if you don't trust the people on your local network.) 54 | 55 | Any TCP session you initiate to one of the proxied IP addresses will be 56 | captured by sshuttle and sent over an ssh session to the remote copy of 57 | sshuttle, which will then regenerate the connection on that end, and funnel 58 | the data back and forth through ssh. 59 | 60 | Fun, right? A poor man's instant VPN, and you don't even have to have 61 | admin access on the server. 62 | 63 | -------------------------------------------------------------------------------- /docs/windows.rst: -------------------------------------------------------------------------------- 1 | Microsoft Windows 2 | ================= 3 | Currently there is no built in support for running sshuttle directly on 4 | Microsoft Windows. 5 | 6 | What we can really do is to create a Linux VM with Vagrant (or simply 7 | Virtualbox if you like). In the Vagrant settings, remember to turn on bridged 8 | NIC. Then, run sshuttle inside the VM like below:: 9 | 10 | sshuttle -l 0.0.0.0 -x 10.0.0.0/8 -x 192.168.0.0/16 0/0 11 | 12 | 10.0.0.0/8 excludes NAT traffic of Vagrant and 192.168.0.0/16 excludes 13 | traffic to local area network (assuming that we're using 192.168.0.0 subnet). 14 | 15 | Assuming the VM has the IP 192.168.1.200 obtained on the bridge NIC (we can 16 | configure that in Vagrant), we can then ask Windows to route all its traffic 17 | via the VM by running the following in cmd.exe with admin right:: 18 | 19 | route add 0.0.0.0 mask 0.0.0.0 192.168.1.200 20 | -------------------------------------------------------------------------------- /requirements-tests.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest==3.4.2 3 | mock==2.0.0 4 | flake8==3.5.0 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools-scm==1.15.6 2 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | export PYTHONPATH="$(dirname $0):$PYTHONPATH" 4 | 5 | python_best_version() { 6 | if [ -x "$(command -v python3)" ] && 7 | python3 -c "import sys; sys.exit(not sys.version_info > (3, 5))"; then 8 | exec python3 "$@" 9 | elif [ -x "$(command -v python2.7)" ]; then 10 | exec python2.7 "$@" 11 | else 12 | exec python "$@" 13 | fi 14 | } 15 | 16 | python_best_version -m "sshuttle" "$@" 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | 7 | [upload] 8 | sign=true 9 | identity=0x1784577F811F6EAC 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012-2014 Brian May 4 | # 5 | # This file is part of sshuttle. 6 | # 7 | # sshuttle is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as 9 | # published by the Free Software Foundation; either version 2.1 of 10 | # the License, or (at your option) any later version. 11 | # 12 | # sshuttle is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with sshuttle; If not, see . 19 | 20 | from setuptools import setup, find_packages 21 | 22 | 23 | def version_scheme(version): 24 | from setuptools_scm.version import guess_next_dev_version 25 | version = guess_next_dev_version(version) 26 | return version.lstrip("v") 27 | 28 | 29 | setup( 30 | name="sshuttle", 31 | use_scm_version={ 32 | 'write_to': "sshuttle/version.py", 33 | 'version_scheme': version_scheme, 34 | }, 35 | setup_requires=['setuptools_scm'], 36 | # version=version, 37 | url='https://github.com/sshuttle/sshuttle', 38 | author='Brian May', 39 | author_email='brian@linuxpenguins.xyz', 40 | description='Full-featured" VPN over an SSH tunnel', 41 | packages=find_packages(), 42 | license="LGPL2.1+", 43 | long_description=open('README.rst').read(), 44 | classifiers=[ 45 | "Development Status :: 5 - Production/Stable", 46 | "Intended Audience :: Developers", 47 | "Intended Audience :: End Users/Desktop", 48 | "License :: OSI Approved :: " 49 | "GNU Lesser General Public License v2 or later (LGPLv2+)", 50 | "Operating System :: OS Independent", 51 | "Programming Language :: Python :: 2.7", 52 | "Programming Language :: Python :: 3.5", 53 | "Topic :: System :: Networking", 54 | ], 55 | entry_points={ 56 | 'console_scripts': [ 57 | 'sshuttle = sshuttle.cmdline:main', 58 | ], 59 | }, 60 | tests_require=['pytest', 'pytest-runner', 'mock'], 61 | keywords="ssh vpn", 62 | ) 63 | -------------------------------------------------------------------------------- /sshuttle/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from sshuttle.version import version as __version__ 3 | except ImportError: 4 | __version__ = "unknown" 5 | -------------------------------------------------------------------------------- /sshuttle/__main__.py: -------------------------------------------------------------------------------- 1 | """Coverage.py's main entry point.""" 2 | import sys 3 | from sshuttle.cmdline import main 4 | sys.exit(main()) 5 | -------------------------------------------------------------------------------- /sshuttle/assembler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import zlib 3 | import imp 4 | 5 | verbosity = verbosity # noqa: F821 must be a previously defined global 6 | z = zlib.decompressobj() 7 | while 1: 8 | name = sys.stdin.readline().strip() 9 | if name: 10 | name = name.decode("ASCII") 11 | 12 | nbytes = int(sys.stdin.readline()) 13 | if verbosity >= 2: 14 | sys.stderr.write('server: assembling %r (%d bytes)\n' 15 | % (name, nbytes)) 16 | content = z.decompress(sys.stdin.read(nbytes)) 17 | 18 | module = imp.new_module(name) 19 | parents = name.rsplit(".", 1) 20 | if len(parents) == 2: 21 | parent, parent_name = parents 22 | setattr(sys.modules[parent], parent_name, module) 23 | 24 | code = compile(content, name, "exec") 25 | exec(code, module.__dict__) # nosec 26 | sys.modules[name] = module 27 | else: 28 | break 29 | 30 | sys.stderr.flush() 31 | sys.stdout.flush() 32 | 33 | import sshuttle.helpers 34 | sshuttle.helpers.verbose = verbosity 35 | 36 | import sshuttle.cmdline_options as options 37 | from sshuttle.server import main 38 | main(options.latency_control, options.auto_hosts, options.to_nameserver) 39 | -------------------------------------------------------------------------------- /sshuttle/cmdline.py: -------------------------------------------------------------------------------- 1 | import re 2 | import socket 3 | import sshuttle.helpers as helpers 4 | import sshuttle.client as client 5 | import sshuttle.firewall as firewall 6 | import sshuttle.hostwatch as hostwatch 7 | import sshuttle.ssyslog as ssyslog 8 | from sshuttle.options import parser, parse_ipport 9 | from sshuttle.helpers import family_ip_tuple, log, Fatal 10 | 11 | 12 | def main(): 13 | opt = parser.parse_args() 14 | 15 | if opt.daemon: 16 | opt.syslog = 1 17 | if opt.wrap: 18 | import sshuttle.ssnet as ssnet 19 | ssnet.MAX_CHANNEL = opt.wrap 20 | helpers.verbose = opt.verbose 21 | 22 | try: 23 | if opt.firewall: 24 | if opt.subnets or opt.subnets_file: 25 | parser.error('exactly zero arguments expected') 26 | return firewall.main(opt.method, opt.syslog) 27 | elif opt.hostwatch: 28 | return hostwatch.hw_main(opt.subnets, opt.auto_hosts) 29 | else: 30 | includes = opt.subnets + opt.subnets_file 31 | excludes = opt.exclude 32 | if not includes and not opt.auto_nets: 33 | parser.error('at least one subnet, subnet file, ' 34 | 'or -N expected') 35 | remotename = opt.remote 36 | if remotename == '' or remotename == '-': 37 | remotename = None 38 | nslist = [family_ip_tuple(ns) for ns in opt.ns_hosts] 39 | if opt.seed_hosts: 40 | sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip()) 41 | elif opt.auto_hosts: 42 | sh = [] 43 | else: 44 | sh = None 45 | if opt.listen: 46 | ipport_v6 = None 47 | ipport_v4 = None 48 | lst = opt.listen.split(",") 49 | for ip in lst: 50 | family, ip, port = parse_ipport(ip) 51 | if family == socket.AF_INET6: 52 | ipport_v6 = (ip, port) 53 | else: 54 | ipport_v4 = (ip, port) 55 | else: 56 | # parse_ipport4('127.0.0.1:0') 57 | ipport_v4 = "auto" 58 | # parse_ipport6('[::1]:0') 59 | ipport_v6 = "auto" if not opt.disable_ipv6 else None 60 | if opt.syslog: 61 | ssyslog.start_syslog() 62 | ssyslog.stderr_to_syslog() 63 | return_code = client.main(ipport_v6, ipport_v4, 64 | opt.ssh_cmd, 65 | remotename, 66 | opt.python, 67 | opt.latency_control, 68 | opt.dns, 69 | nslist, 70 | opt.method, 71 | sh, 72 | opt.auto_hosts, 73 | opt.auto_nets, 74 | includes, 75 | excludes, 76 | opt.daemon, 77 | opt.to_ns, 78 | opt.pidfile, 79 | opt.user) 80 | 81 | if return_code == 0: 82 | log('Normal exit code, exiting...') 83 | else: 84 | log('Abnormal exit code detected, failing...' % return_code) 85 | return return_code 86 | 87 | except Fatal as e: 88 | log('fatal: %s\n' % e) 89 | return 99 90 | except KeyboardInterrupt: 91 | log('\n') 92 | log('Keyboard interrupt: exiting.\n') 93 | return 1 94 | -------------------------------------------------------------------------------- /sshuttle/firewall.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import socket 3 | import signal 4 | import sshuttle.ssyslog as ssyslog 5 | import sshuttle.sdnotify as sdnotify 6 | import sys 7 | import os 8 | import platform 9 | import traceback 10 | from sshuttle.helpers import debug1, debug2, Fatal 11 | from sshuttle.methods import get_auto_method, get_method 12 | 13 | HOSTSFILE = '/etc/hosts' 14 | 15 | 16 | def rewrite_etc_hosts(hostmap, port): 17 | BAKFILE = '%s.sbak' % HOSTSFILE 18 | APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port 19 | old_content = '' 20 | st = None 21 | try: 22 | old_content = open(HOSTSFILE).read() 23 | st = os.stat(HOSTSFILE) 24 | except IOError as e: 25 | if e.errno == errno.ENOENT: 26 | pass 27 | else: 28 | raise 29 | if old_content.strip() and not os.path.exists(BAKFILE): 30 | os.link(HOSTSFILE, BAKFILE) 31 | tmpname = "%s.%d.tmp" % (HOSTSFILE, port) 32 | f = open(tmpname, 'w') 33 | for line in old_content.rstrip().split('\n'): 34 | if line.find(APPEND) >= 0: 35 | continue 36 | f.write('%s\n' % line) 37 | for (name, ip) in sorted(hostmap.items()): 38 | f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND)) 39 | f.close() 40 | 41 | if st is not None: 42 | os.chown(tmpname, st.st_uid, st.st_gid) 43 | os.chmod(tmpname, st.st_mode) 44 | else: 45 | os.chown(tmpname, 0, 0) 46 | os.chmod(tmpname, 0o600) 47 | os.rename(tmpname, HOSTSFILE) 48 | 49 | 50 | def restore_etc_hosts(port): 51 | rewrite_etc_hosts({}, port) 52 | 53 | 54 | # Isolate function that needs to be replaced for tests 55 | def setup_daemon(): 56 | if os.getuid() != 0: 57 | raise Fatal('you must be root (or enable su/sudo) to set the firewall') 58 | 59 | # don't disappear if our controlling terminal or stdout/stderr 60 | # disappears; we still have to clean up. 61 | signal.signal(signal.SIGHUP, signal.SIG_IGN) 62 | signal.signal(signal.SIGPIPE, signal.SIG_IGN) 63 | signal.signal(signal.SIGTERM, signal.SIG_IGN) 64 | signal.signal(signal.SIGINT, signal.SIG_IGN) 65 | 66 | # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, 67 | # I'll die automatically. 68 | os.setsid() 69 | 70 | # because of limitations of the 'su' command, the *real* stdin/stdout 71 | # are both attached to stdout initially. Clone stdout into stdin so we 72 | # can read from it. 73 | os.dup2(1, 0) 74 | 75 | return sys.stdin, sys.stdout 76 | 77 | 78 | # Note that we're sorting in a very particular order: 79 | # we need to go from smaller, more specific, port ranges, to larger, 80 | # less-specific, port ranges. At each level, we order by subnet 81 | # width, from most-specific subnets (largest swidth) to 82 | # least-specific. On ties, excludes come first. 83 | # s:(inet, subnet width, exclude flag, subnet, first port, last port) 84 | def subnet_weight(s): 85 | return (-s[-1] + (s[-2] or -65535), s[1], s[2]) 86 | 87 | 88 | # This is some voodoo for setting up the kernel's transparent 89 | # proxying stuff. If subnets is empty, we just delete our sshuttle rules; 90 | # otherwise we delete it, then make them from scratch. 91 | # 92 | # This code is supposed to clean up after itself by deleting its rules on 93 | # exit. In case that fails, it's not the end of the world; future runs will 94 | # supercede it in the transproxy list, at least, so the leftover rules 95 | # are hopefully harmless. 96 | def main(method_name, syslog): 97 | stdin, stdout = setup_daemon() 98 | hostmap = {} 99 | 100 | debug1('firewall manager: Starting firewall with Python version %s\n' 101 | % platform.python_version()) 102 | 103 | if method_name == "auto": 104 | method = get_auto_method() 105 | else: 106 | method = get_method(method_name) 107 | 108 | if syslog: 109 | ssyslog.start_syslog() 110 | ssyslog.stderr_to_syslog() 111 | 112 | debug1('firewall manager: ready method name %s.\n' % method.name) 113 | stdout.write('READY %s\n' % method.name) 114 | stdout.flush() 115 | 116 | # we wait until we get some input before creating the rules. That way, 117 | # sshuttle can launch us as early as possible (and get sudo password 118 | # authentication as early in the startup process as possible). 119 | line = stdin.readline(128) 120 | if not line: 121 | return # parent died; nothing to do 122 | 123 | subnets = [] 124 | if line != 'ROUTES\n': 125 | raise Fatal('firewall: expected ROUTES but got %r' % line) 126 | while 1: 127 | line = stdin.readline(128) 128 | if not line: 129 | raise Fatal('firewall: expected route but got %r' % line) 130 | elif line.startswith("NSLIST\n"): 131 | break 132 | try: 133 | (family, width, exclude, ip, fport, lport) = \ 134 | line.strip().split(',', 5) 135 | except: 136 | raise Fatal('firewall: expected route or NSLIST but got %r' % line) 137 | subnets.append(( 138 | int(family), 139 | int(width), 140 | bool(int(exclude)), 141 | ip, 142 | int(fport), 143 | int(lport))) 144 | debug2('firewall manager: Got subnets: %r\n' % subnets) 145 | 146 | nslist = [] 147 | if line != 'NSLIST\n': 148 | raise Fatal('firewall: expected NSLIST but got %r' % line) 149 | while 1: 150 | line = stdin.readline(128) 151 | if not line: 152 | raise Fatal('firewall: expected nslist but got %r' % line) 153 | elif line.startswith("PORTS "): 154 | break 155 | try: 156 | (family, ip) = line.strip().split(',', 1) 157 | except: 158 | raise Fatal('firewall: expected nslist or PORTS but got %r' % line) 159 | nslist.append((int(family), ip)) 160 | debug2('firewall manager: Got partial nslist: %r\n' % nslist) 161 | debug2('firewall manager: Got nslist: %r\n' % nslist) 162 | 163 | if not line.startswith('PORTS '): 164 | raise Fatal('firewall: expected PORTS but got %r' % line) 165 | _, _, ports = line.partition(" ") 166 | ports = ports.split(",") 167 | if len(ports) != 4: 168 | raise Fatal('firewall: expected 4 ports but got %d' % len(ports)) 169 | port_v6 = int(ports[0]) 170 | port_v4 = int(ports[1]) 171 | dnsport_v6 = int(ports[2]) 172 | dnsport_v4 = int(ports[3]) 173 | 174 | assert(port_v6 >= 0) 175 | assert(port_v6 <= 65535) 176 | assert(port_v4 >= 0) 177 | assert(port_v4 <= 65535) 178 | assert(dnsport_v6 >= 0) 179 | assert(dnsport_v6 <= 65535) 180 | assert(dnsport_v4 >= 0) 181 | assert(dnsport_v4 <= 65535) 182 | 183 | debug2('firewall manager: Got ports: %d,%d,%d,%d\n' 184 | % (port_v6, port_v4, dnsport_v6, dnsport_v4)) 185 | 186 | line = stdin.readline(128) 187 | if not line: 188 | raise Fatal('firewall: expected GO but got %r' % line) 189 | elif not line.startswith("GO "): 190 | raise Fatal('firewall: expected GO but got %r' % line) 191 | 192 | _, _, args = line.partition(" ") 193 | udp, user = args.strip().split(" ", 1) 194 | udp = bool(int(udp)) 195 | if user == '-': 196 | user = None 197 | debug2('firewall manager: Got udp: %r, user: %r\n' % (udp, user)) 198 | 199 | subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] 200 | nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] 201 | subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] 202 | nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] 203 | 204 | try: 205 | debug1('firewall manager: setting up.\n') 206 | 207 | if subnets_v6 or nslist_v6: 208 | debug2('firewall manager: setting up IPv6.\n') 209 | method.setup_firewall( 210 | port_v6, dnsport_v6, nslist_v6, 211 | socket.AF_INET6, subnets_v6, udp, 212 | user) 213 | 214 | if subnets_v4 or nslist_v4: 215 | debug2('firewall manager: setting up IPv4.\n') 216 | method.setup_firewall( 217 | port_v4, dnsport_v4, nslist_v4, 218 | socket.AF_INET, subnets_v4, udp, 219 | user) 220 | 221 | stdout.write('STARTED\n') 222 | sdnotify.send(sdnotify.ready(), 223 | sdnotify.status('Connected')) 224 | 225 | try: 226 | stdout.flush() 227 | except IOError: 228 | # the parent process died for some reason; he's surely been loud 229 | # enough, so no reason to report another error 230 | return 231 | 232 | # Now we wait until EOF or any other kind of exception. We need 233 | # to stay running so that we don't need a *second* password 234 | # authentication at shutdown time - that cleanup is important! 235 | while 1: 236 | line = stdin.readline(128) 237 | if line.startswith('HOST '): 238 | (name, ip) = line[5:].strip().split(',', 1) 239 | hostmap[name] = ip 240 | debug2('firewall manager: setting up /etc/hosts.\n') 241 | rewrite_etc_hosts(hostmap, port_v6 or port_v4) 242 | elif line: 243 | if not method.firewall_command(line): 244 | raise Fatal('firewall: expected command, got %r' % line) 245 | else: 246 | break 247 | finally: 248 | try: 249 | sdnotify.send(sdnotify.stop()) 250 | debug1('firewall manager: undoing changes.\n') 251 | except: 252 | pass 253 | 254 | try: 255 | if subnets_v6 or nslist_v6: 256 | debug2('firewall manager: undoing IPv6 changes.\n') 257 | method.restore_firewall(port_v6, socket.AF_INET6, udp, user) 258 | except: 259 | try: 260 | debug1("firewall manager: " 261 | "Error trying to undo IPv6 firewall.\n") 262 | for line in traceback.format_exc().splitlines(): 263 | debug1("---> %s\n" % line) 264 | except: 265 | pass 266 | 267 | try: 268 | if subnets_v4 or nslist_v4: 269 | debug2('firewall manager: undoing IPv4 changes.\n') 270 | method.restore_firewall(port_v4, socket.AF_INET, udp, user) 271 | except: 272 | try: 273 | debug1("firewall manager: " 274 | "Error trying to undo IPv4 firewall.\n") 275 | for line in traceback.format_exc().splitlines(): 276 | debug1("firewall manager: ---> %s\n" % line) 277 | except: 278 | pass 279 | 280 | try: 281 | debug2('firewall manager: undoing /etc/hosts changes.\n') 282 | restore_etc_hosts(port_v6 or port_v4) 283 | except: 284 | try: 285 | debug1("firewall manager: " 286 | "Error trying to undo /etc/hosts changes.\n") 287 | for line in traceback.format_exc().splitlines(): 288 | debug1("firewall manager: ---> %s\n" % line) 289 | except: 290 | pass 291 | -------------------------------------------------------------------------------- /sshuttle/helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import socket 3 | import errno 4 | 5 | logprefix = '' 6 | verbose = 0 7 | 8 | if sys.version_info[0] == 3: 9 | binary_type = bytes 10 | 11 | def b(s): 12 | return s.encode("ASCII") 13 | else: 14 | binary_type = str 15 | 16 | def b(s): 17 | return s 18 | 19 | 20 | def log(s): 21 | global logprefix 22 | try: 23 | sys.stdout.flush() 24 | if s.find("\n") != -1: 25 | prefix = logprefix 26 | s = s.rstrip("\n") 27 | for line in s.split("\n"): 28 | sys.stderr.write(prefix + line + "\n") 29 | prefix = "---> " 30 | else: 31 | sys.stderr.write(logprefix + s) 32 | sys.stderr.flush() 33 | except IOError: 34 | # this could happen if stderr gets forcibly disconnected, eg. because 35 | # our tty closes. That sucks, but it's no reason to abort the program. 36 | pass 37 | 38 | 39 | def debug1(s): 40 | if verbose >= 1: 41 | log(s) 42 | 43 | 44 | def debug2(s): 45 | if verbose >= 2: 46 | log(s) 47 | 48 | 49 | def debug3(s): 50 | if verbose >= 3: 51 | log(s) 52 | 53 | 54 | class Fatal(Exception): 55 | pass 56 | 57 | 58 | def resolvconf_nameservers(): 59 | l = [] 60 | for line in open('/etc/resolv.conf'): 61 | words = line.lower().split() 62 | if len(words) >= 2 and words[0] == 'nameserver': 63 | l.append(family_ip_tuple(words[1])) 64 | return l 65 | 66 | 67 | def resolvconf_random_nameserver(): 68 | l = resolvconf_nameservers() 69 | if l: 70 | if len(l) > 1: 71 | # don't import this unless we really need it 72 | import random 73 | random.shuffle(l) 74 | return l[0] 75 | else: 76 | return (socket.AF_INET, '127.0.0.1') 77 | 78 | 79 | def islocal(ip, family): 80 | sock = socket.socket(family) 81 | try: 82 | try: 83 | sock.bind((ip, 0)) 84 | except socket.error: 85 | _, e = sys.exc_info()[:2] 86 | if e.args[0] == errno.EADDRNOTAVAIL: 87 | return False # not a local IP 88 | else: 89 | raise 90 | finally: 91 | sock.close() 92 | return True # it's a local IP, or there would have been an error 93 | 94 | 95 | def family_ip_tuple(ip): 96 | if ':' in ip: 97 | return (socket.AF_INET6, ip) 98 | else: 99 | return (socket.AF_INET, ip) 100 | 101 | 102 | def family_to_string(family): 103 | if family == socket.AF_INET6: 104 | return "AF_INET6" 105 | elif family == socket.AF_INET: 106 | return "AF_INET" 107 | else: 108 | return str(family) 109 | -------------------------------------------------------------------------------- /sshuttle/hostwatch.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socket 3 | import re 4 | import select 5 | import errno 6 | import os 7 | import sys 8 | import platform 9 | 10 | import subprocess as ssubprocess 11 | import sshuttle.helpers as helpers 12 | from sshuttle.helpers import log, debug1, debug2, debug3 13 | 14 | POLL_TIME = 60 * 15 15 | NETSTAT_POLL_TIME = 30 16 | CACHEFILE = os.path.expanduser('~/.sshuttle.hosts') 17 | 18 | 19 | _nmb_ok = True 20 | _smb_ok = True 21 | hostnames = {} 22 | queue = {} 23 | try: 24 | null = open('/dev/null', 'wb') 25 | except IOError: 26 | _, e = sys.exc_info()[:2] 27 | log('warning: %s\n' % e) 28 | null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096) 29 | 30 | 31 | def _is_ip(s): 32 | return re.match(r'\d+\.\d+\.\d+\.\d+$', s) 33 | 34 | 35 | def write_host_cache(): 36 | tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid()) 37 | try: 38 | f = open(tmpname, 'wb') 39 | for name, ip in sorted(hostnames.items()): 40 | f.write(('%s,%s\n' % (name, ip)).encode("ASCII")) 41 | f.close() 42 | os.chmod(tmpname, 384) # 600 in octal, 'rw-------' 43 | os.rename(tmpname, CACHEFILE) 44 | finally: 45 | try: 46 | os.unlink(tmpname) 47 | except: 48 | pass 49 | 50 | 51 | def read_host_cache(): 52 | try: 53 | f = open(CACHEFILE) 54 | except IOError: 55 | _, e = sys.exc_info()[:2] 56 | if e.errno == errno.ENOENT: 57 | return 58 | else: 59 | raise 60 | for line in f: 61 | words = line.strip().split(',') 62 | if len(words) == 2: 63 | (name, ip) = words 64 | name = re.sub(r'[^-\w\.]', '-', name).strip() 65 | ip = re.sub(r'[^0-9.]', '', ip).strip() 66 | if name and ip: 67 | found_host(name, ip) 68 | 69 | 70 | def found_host(name, ip): 71 | hostname = re.sub(r'\..*', '', name) 72 | hostname = re.sub(r'[^-\w\.]', '_', hostname) 73 | if (ip.startswith('127.') or ip.startswith('255.') or 74 | hostname == 'localhost'): 75 | return 76 | 77 | if hostname != name: 78 | found_host(hostname, ip) 79 | 80 | oldip = hostnames.get(name) 81 | if oldip != ip: 82 | hostnames[name] = ip 83 | debug1('Found: %s: %s\n' % (name, ip)) 84 | sys.stdout.write('%s,%s\n' % (name, ip)) 85 | write_host_cache() 86 | 87 | 88 | def _check_etc_hosts(): 89 | debug2(' > hosts\n') 90 | for line in open('/etc/hosts'): 91 | line = re.sub(r'#.*', '', line) 92 | words = line.strip().split() 93 | if not words: 94 | continue 95 | ip = words[0] 96 | names = words[1:] 97 | if _is_ip(ip): 98 | debug3('< %s %r\n' % (ip, names)) 99 | for n in names: 100 | check_host(n) 101 | found_host(n, ip) 102 | 103 | 104 | def _check_revdns(ip): 105 | debug2(' > rev: %s\n' % ip) 106 | try: 107 | r = socket.gethostbyaddr(ip) 108 | debug3('< %s\n' % r[0]) 109 | check_host(r[0]) 110 | found_host(r[0], ip) 111 | except socket.herror: 112 | pass 113 | 114 | 115 | def _check_dns(hostname): 116 | debug2(' > dns: %s\n' % hostname) 117 | try: 118 | ip = socket.gethostbyname(hostname) 119 | debug3('< %s\n' % ip) 120 | check_host(ip) 121 | found_host(hostname, ip) 122 | except socket.gaierror: 123 | pass 124 | 125 | 126 | def _check_netstat(): 127 | debug2(' > netstat\n') 128 | argv = ['netstat', '-n'] 129 | try: 130 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) 131 | content = p.stdout.read().decode("ASCII") 132 | p.wait() 133 | except OSError: 134 | _, e = sys.exc_info()[:2] 135 | log('%r failed: %r\n' % (argv, e)) 136 | return 137 | 138 | for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content): 139 | debug3('< %s\n' % ip) 140 | check_host(ip) 141 | 142 | 143 | def _check_smb(hostname): 144 | return 145 | global _smb_ok 146 | if not _smb_ok: 147 | return 148 | argv = ['smbclient', '-U', '%', '-L', hostname] 149 | debug2(' > smb: %s\n' % hostname) 150 | try: 151 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) 152 | lines = p.stdout.readlines() 153 | p.wait() 154 | except OSError: 155 | _, e = sys.exc_info()[:2] 156 | log('%r failed: %r\n' % (argv, e)) 157 | _smb_ok = False 158 | return 159 | 160 | lines.reverse() 161 | 162 | # junk at top 163 | while lines: 164 | line = lines.pop().strip() 165 | if re.match(r'Server\s+', line): 166 | break 167 | 168 | # server list section: 169 | # Server Comment 170 | # ------ ------- 171 | while lines: 172 | line = lines.pop().strip() 173 | if not line or re.match(r'-+\s+-+', line): 174 | continue 175 | if re.match(r'Workgroup\s+Master', line): 176 | break 177 | words = line.split() 178 | hostname = words[0].lower() 179 | debug3('< %s\n' % hostname) 180 | check_host(hostname) 181 | 182 | # workgroup list section: 183 | # Workgroup Master 184 | # --------- ------ 185 | while lines: 186 | line = lines.pop().strip() 187 | if re.match(r'-+\s+', line): 188 | continue 189 | if not line: 190 | break 191 | words = line.split() 192 | (workgroup, hostname) = (words[0].lower(), words[1].lower()) 193 | debug3('< group(%s) -> %s\n' % (workgroup, hostname)) 194 | check_host(hostname) 195 | check_workgroup(workgroup) 196 | 197 | if lines: 198 | assert(0) 199 | 200 | 201 | def _check_nmb(hostname, is_workgroup, is_master): 202 | return 203 | global _nmb_ok 204 | if not _nmb_ok: 205 | return 206 | argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname] 207 | debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname)) 208 | try: 209 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) 210 | lines = p.stdout.readlines() 211 | rv = p.wait() 212 | except OSError: 213 | _, e = sys.exc_info()[:2] 214 | log('%r failed: %r\n' % (argv, e)) 215 | _nmb_ok = False 216 | return 217 | if rv: 218 | log('%r returned %d\n' % (argv, rv)) 219 | return 220 | for line in lines: 221 | m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line) 222 | if m: 223 | g = m.groups() 224 | (ip, name) = (g[0], g[1].lower()) 225 | debug3('< %s -> %s\n' % (name, ip)) 226 | if is_workgroup: 227 | _enqueue(_check_smb, ip) 228 | else: 229 | found_host(name, ip) 230 | check_host(name) 231 | 232 | 233 | def check_host(hostname): 234 | if _is_ip(hostname): 235 | _enqueue(_check_revdns, hostname) 236 | else: 237 | _enqueue(_check_dns, hostname) 238 | _enqueue(_check_smb, hostname) 239 | _enqueue(_check_nmb, hostname, False, False) 240 | 241 | 242 | def check_workgroup(hostname): 243 | _enqueue(_check_nmb, hostname, True, False) 244 | _enqueue(_check_nmb, hostname, True, True) 245 | 246 | 247 | def _enqueue(op, *args): 248 | t = (op, args) 249 | if queue.get(t) is None: 250 | queue[t] = 0 251 | 252 | 253 | def _stdin_still_ok(timeout): 254 | r, _, _ = select.select([sys.stdin.fileno()], [], [], timeout) 255 | if r: 256 | b = os.read(sys.stdin.fileno(), 4096) 257 | if not b: 258 | return False 259 | return True 260 | 261 | 262 | def hw_main(seed_hosts, auto_hosts): 263 | if helpers.verbose >= 2: 264 | helpers.logprefix = 'HH: ' 265 | else: 266 | helpers.logprefix = 'hostwatch: ' 267 | 268 | debug1('Starting hostwatch with Python version %s\n' 269 | % platform.python_version()) 270 | 271 | for h in seed_hosts: 272 | check_host(h) 273 | 274 | if auto_hosts: 275 | read_host_cache() 276 | _enqueue(_check_etc_hosts) 277 | _enqueue(_check_netstat) 278 | check_host('localhost') 279 | check_host(socket.gethostname()) 280 | check_workgroup('workgroup') 281 | check_workgroup('-') 282 | 283 | while 1: 284 | now = time.time() 285 | for t, last_polled in list(queue.items()): 286 | (op, args) = t 287 | if not _stdin_still_ok(0): 288 | break 289 | maxtime = POLL_TIME 290 | if op == _check_netstat: 291 | maxtime = NETSTAT_POLL_TIME 292 | if now - last_polled > maxtime: 293 | queue[t] = time.time() 294 | op(*args) 295 | try: 296 | sys.stdout.flush() 297 | except IOError: 298 | break 299 | 300 | # FIXME: use a smarter timeout based on oldest last_polled 301 | if not _stdin_still_ok(1): 302 | break 303 | -------------------------------------------------------------------------------- /sshuttle/linux.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import socket 4 | import subprocess as ssubprocess 5 | from sshuttle.helpers import log, debug1, Fatal, family_to_string 6 | 7 | 8 | def nonfatal(func, *args): 9 | try: 10 | func(*args) 11 | except Fatal as e: 12 | log('error: %s\n' % e) 13 | 14 | 15 | def ipt_chain_exists(family, table, name): 16 | if family == socket.AF_INET6: 17 | cmd = 'ip6tables' 18 | elif family == socket.AF_INET: 19 | cmd = 'iptables' 20 | else: 21 | raise Exception('Unsupported family "%s"' % family_to_string(family)) 22 | argv = [cmd, '-t', table, '-nL'] 23 | env = { 24 | 'PATH': os.environ['PATH'], 25 | 'LC_ALL': "C", 26 | } 27 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env) 28 | for line in p.stdout: 29 | if line.startswith(b'Chain %s ' % name.encode("ASCII")): 30 | return True 31 | rv = p.wait() 32 | if rv: 33 | raise Fatal('%r returned %d' % (argv, rv)) 34 | 35 | 36 | def ipt(family, table, *args): 37 | if family == socket.AF_INET6: 38 | argv = ['ip6tables', '-t', table] + list(args) 39 | elif family == socket.AF_INET: 40 | argv = ['iptables', '-t', table] + list(args) 41 | else: 42 | raise Exception('Unsupported family "%s"' % family_to_string(family)) 43 | debug1('>> %s\n' % ' '.join(argv)) 44 | env = { 45 | 'PATH': os.environ['PATH'], 46 | 'LC_ALL': "C", 47 | } 48 | rv = ssubprocess.call(argv, env=env) 49 | if rv: 50 | raise Fatal('%r returned %d' % (argv, rv)) 51 | 52 | 53 | def nft(family, table, action, *args): 54 | if family == socket.AF_INET: 55 | argv = ['nft', action, 'ip', table] + list(args) 56 | elif family == socket.AF_INET6: 57 | argv = ['nft', action, 'ip6', table] + list(args) 58 | else: 59 | raise Exception('Unsupported family "%s"' % family_to_string(family)) 60 | debug1('>> %s\n' % ' '.join(argv)) 61 | env = { 62 | 'PATH': os.environ['PATH'], 63 | 'LC_ALL': "C", 64 | } 65 | rv = ssubprocess.call(argv, env=env) 66 | if rv: 67 | raise Fatal('%r returned %d' % (argv, rv)) 68 | 69 | 70 | def nft_get_handle(expression, chain): 71 | cmd = 'nft' 72 | argv = [cmd, 'list', expression, '-a'] 73 | env = { 74 | 'PATH': os.environ['PATH'], 75 | 'LC_ALL': "C", 76 | } 77 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env) 78 | for line in p.stdout: 79 | if (b'jump %s' % chain.encode('utf-8')) in line: 80 | return re.sub('.*# ', '', line.decode('utf-8')) 81 | rv = p.wait() 82 | if rv: 83 | raise Fatal('%r returned %d' % (argv, rv)) 84 | 85 | 86 | _no_ttl_module = False 87 | 88 | 89 | def ipt_ttl(family, *args): 90 | global _no_ttl_module 91 | if not _no_ttl_module: 92 | # we avoid infinite loops by generating server-side connections 93 | # with ttl 42. This makes the client side not recapture those 94 | # connections, in case client == server. 95 | try: 96 | argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] 97 | ipt(family, *argsplus) 98 | except Fatal: 99 | ipt(family, *args) 100 | # we only get here if the non-ttl attempt succeeds 101 | log('sshuttle: warning: your iptables is missing ' 102 | 'the ttl module.\n') 103 | _no_ttl_module = True 104 | else: 105 | ipt(family, *args) 106 | -------------------------------------------------------------------------------- /sshuttle/methods/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import socket 4 | import struct 5 | import errno 6 | from sshuttle.helpers import Fatal, debug3 7 | 8 | 9 | def original_dst(sock): 10 | try: 11 | SO_ORIGINAL_DST = 80 12 | SOCKADDR_MIN = 16 13 | sockaddr_in = sock.getsockopt(socket.SOL_IP, 14 | SO_ORIGINAL_DST, SOCKADDR_MIN) 15 | (proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) 16 | # FIXME: decoding is IPv4 only. 17 | assert(socket.htons(proto) == socket.AF_INET) 18 | ip = '%d.%d.%d.%d' % (a, b, c, d) 19 | return (ip, port) 20 | except socket.error as e: 21 | if e.args[0] == errno.ENOPROTOOPT: 22 | return sock.getsockname() 23 | raise 24 | 25 | 26 | class Features(object): 27 | pass 28 | 29 | 30 | class BaseMethod(object): 31 | def __init__(self, name): 32 | self.firewall = None 33 | self.name = name 34 | 35 | def set_firewall(self, firewall): 36 | self.firewall = firewall 37 | 38 | @staticmethod 39 | def get_supported_features(): 40 | result = Features() 41 | result.ipv6 = False 42 | result.udp = False 43 | result.dns = True 44 | result.user = False 45 | return result 46 | 47 | @staticmethod 48 | def get_tcp_dstip(sock): 49 | return original_dst(sock) 50 | 51 | @staticmethod 52 | def recv_udp(udp_listener, bufsize): 53 | debug3('Accept UDP using recvfrom.\n') 54 | data, srcip = udp_listener.recvfrom(bufsize) 55 | return (srcip, None, data) 56 | 57 | def send_udp(self, sock, srcip, dstip, data): 58 | if srcip is not None: 59 | Fatal("Method %s send_udp does not support setting srcip to %r" 60 | % (self.name, srcip)) 61 | sock.sendto(data, dstip) 62 | 63 | def setup_tcp_listener(self, tcp_listener): 64 | pass 65 | 66 | def setup_udp_listener(self, udp_listener): 67 | pass 68 | 69 | def assert_features(self, features): 70 | avail = self.get_supported_features() 71 | for key in ["udp", "dns", "ipv6", "user"]: 72 | if getattr(features, key) and not getattr(avail, key): 73 | raise Fatal( 74 | "Feature %s not supported with method %s.\n" % 75 | (key, self.name)) 76 | 77 | def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, 78 | user): 79 | raise NotImplementedError() 80 | 81 | def restore_firewall(self, port, family, udp, user): 82 | raise NotImplementedError() 83 | 84 | @staticmethod 85 | def firewall_command(line): 86 | return False 87 | 88 | 89 | def _program_exists(name): 90 | paths = (os.getenv('PATH') or os.defpath).split(os.pathsep) 91 | for p in paths: 92 | fn = '%s/%s' % (p, name) 93 | if os.path.exists(fn): 94 | return not os.path.isdir(fn) and os.access(fn, os.X_OK) 95 | 96 | 97 | def get_method(method_name): 98 | module = importlib.import_module("sshuttle.methods.%s" % method_name) 99 | return module.Method(method_name) 100 | 101 | 102 | def get_auto_method(): 103 | if _program_exists('iptables'): 104 | method_name = "nat" 105 | elif _program_exists('nft'): 106 | method_name = "nft" 107 | elif _program_exists('pfctl'): 108 | method_name = "pf" 109 | elif _program_exists('ipfw'): 110 | method_name = "ipfw" 111 | else: 112 | raise Fatal( 113 | "can't find either iptables, nft or pfctl; check your PATH") 114 | 115 | return get_method(method_name) 116 | -------------------------------------------------------------------------------- /sshuttle/methods/ipfw.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess as ssubprocess 3 | from sshuttle.methods import BaseMethod 4 | from sshuttle.helpers import log, debug1, debug3, \ 5 | Fatal, family_to_string 6 | 7 | recvmsg = None 8 | try: 9 | # try getting recvmsg from python 10 | import socket as pythonsocket 11 | getattr(pythonsocket.socket, "recvmsg") 12 | socket = pythonsocket 13 | recvmsg = "python" 14 | except AttributeError: 15 | # try getting recvmsg from socket_ext library 16 | try: 17 | import socket_ext 18 | getattr(socket_ext.socket, "recvmsg") 19 | socket = socket_ext 20 | recvmsg = "socket_ext" 21 | except ImportError: 22 | import socket 23 | 24 | IP_BINDANY = 24 25 | IP_RECVDSTADDR = 7 26 | SOL_IPV6 = 41 27 | IPV6_RECVDSTADDR = 74 28 | 29 | if recvmsg == "python": 30 | def recv_udp(listener, bufsize): 31 | debug3('Accept UDP python using recvmsg.\n') 32 | data, ancdata, _, srcip = \ 33 | listener.recvmsg(4096, socket.CMSG_SPACE(4)) 34 | dstip = None 35 | for cmsg_level, cmsg_type, cmsg_data in ancdata: 36 | if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR: 37 | port = 53 38 | ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4]) 39 | dstip = (ip, port) 40 | break 41 | return (srcip, dstip, data) 42 | elif recvmsg == "socket_ext": 43 | def recv_udp(listener, bufsize): 44 | debug3('Accept UDP using socket_ext recvmsg.\n') 45 | srcip, data, adata, _ = \ 46 | listener.recvmsg((bufsize,), socket.CMSG_SPACE(4)) 47 | dstip = None 48 | for a in adata: 49 | if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR: 50 | port = 53 51 | ip = socket.inet_ntop(socket.AF_INET, a.cmsg_data[0:4]) 52 | dstip = (ip, port) 53 | break 54 | return (srcip, dstip, data[0]) 55 | else: 56 | def recv_udp(listener, bufsize): 57 | debug3('Accept UDP using recvfrom.\n') 58 | data, srcip = listener.recvfrom(bufsize) 59 | return (srcip, None, data) 60 | 61 | 62 | def ipfw_rule_exists(n): 63 | argv = ['ipfw', 'list'] 64 | env = { 65 | 'PATH': os.environ['PATH'], 66 | 'LC_ALL': "C", 67 | } 68 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env) 69 | 70 | found = False 71 | for line in p.stdout: 72 | if line.startswith(b'%05d ' % n): 73 | if not ('ipttl 42' in line or 'check-state' in line): 74 | log('non-sshuttle ipfw rule: %r\n' % line.strip()) 75 | raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n) 76 | found = True 77 | rv = p.wait() 78 | if rv: 79 | raise Fatal('%r returned %d' % (argv, rv)) 80 | return found 81 | 82 | 83 | _oldctls = {} 84 | 85 | 86 | def _fill_oldctls(prefix): 87 | argv = ['sysctl', prefix] 88 | env = { 89 | 'PATH': os.environ['PATH'], 90 | 'LC_ALL': "C", 91 | } 92 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env) 93 | for line in p.stdout: 94 | line = line.decode() 95 | assert(line[-1] == '\n') 96 | (k, v) = line[:-1].split(': ', 1) 97 | _oldctls[k] = v.strip() 98 | rv = p.wait() 99 | if rv: 100 | raise Fatal('%r returned %d' % (argv, rv)) 101 | if not line: 102 | raise Fatal('%r returned no data' % (argv,)) 103 | 104 | 105 | def _sysctl_set(name, val): 106 | argv = ['sysctl', '-w', '%s=%s' % (name, val)] 107 | debug1('>> %s\n' % ' '.join(argv)) 108 | return ssubprocess.call(argv, stdout=open('/dev/null', 'w')) 109 | 110 | 111 | _changedctls = [] 112 | 113 | 114 | def sysctl_set(name, val, permanent=False): 115 | PREFIX = 'net.inet.ip' 116 | assert(name.startswith(PREFIX + '.')) 117 | val = str(val) 118 | if not _oldctls: 119 | _fill_oldctls(PREFIX) 120 | if not (name in _oldctls): 121 | debug1('>> No such sysctl: %r\n' % name) 122 | return False 123 | oldval = _oldctls[name] 124 | if val != oldval: 125 | rv = _sysctl_set(name, val) 126 | if rv == 0 and permanent: 127 | debug1('>> ...saving permanently in /etc/sysctl.conf\n') 128 | f = open('/etc/sysctl.conf', 'a') 129 | f.write('\n' 130 | '# Added by sshuttle\n' 131 | '%s=%s\n' % (name, val)) 132 | f.close() 133 | else: 134 | _changedctls.append(name) 135 | return True 136 | 137 | def ipfw(*args): 138 | argv = ['ipfw', '-q'] + list(args) 139 | debug1('>> %s\n' % ' '.join(argv)) 140 | rv = ssubprocess.call(argv) 141 | if rv: 142 | raise Fatal('%r returned %d' % (argv, rv)) 143 | 144 | 145 | def ipfw_noexit(*args): 146 | argv = ['ipfw', '-q'] + list(args) 147 | debug1('>> %s\n' % ' '.join(argv)) 148 | ssubprocess.call(argv) 149 | 150 | class Method(BaseMethod): 151 | 152 | def get_supported_features(self): 153 | result = super(Method, self).get_supported_features() 154 | result.ipv6 = False 155 | result.udp = False #NOTE: Almost there, kernel patch needed 156 | result.dns = True 157 | return result 158 | 159 | def get_tcp_dstip(self, sock): 160 | return sock.getsockname() 161 | 162 | def recv_udp(self, udp_listener, bufsize): 163 | srcip, dstip, data = recv_udp(udp_listener, bufsize) 164 | if not dstip: 165 | debug1( 166 | "-- ignored UDP from %r: " 167 | "couldn't determine destination IP address\n" % (srcip,)) 168 | return None 169 | return srcip, dstip, data 170 | 171 | def send_udp(self, sock, srcip, dstip, data): 172 | if not srcip: 173 | debug1( 174 | "-- ignored UDP to %r: " 175 | "couldn't determine source IP address\n" % (dstip,)) 176 | return 177 | 178 | #debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip)) 179 | sender = socket.socket(sock.family, socket.SOCK_DGRAM) 180 | sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1) 181 | sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 182 | sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 183 | sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) 184 | sender.bind(srcip) 185 | sender.sendto(data,dstip) 186 | sender.close() 187 | 188 | def setup_udp_listener(self, udp_listener): 189 | if udp_listener.v4 is not None: 190 | udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1) 191 | #if udp_listener.v6 is not None: 192 | # udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1) 193 | 194 | def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, 195 | user): 196 | # IPv6 not supported 197 | if family not in [socket.AF_INET]: 198 | raise Exception( 199 | 'Address family "%s" unsupported by ipfw method_name' 200 | % family_to_string(family)) 201 | 202 | #XXX: Any risk from this? 203 | ipfw_noexit('delete', '1') 204 | 205 | while _changedctls: 206 | name = _changedctls.pop() 207 | oldval = _oldctls[name] 208 | _sysctl_set(name, oldval) 209 | 210 | if subnets or dnsport: 211 | sysctl_set('net.inet.ip.fw.enable', 1) 212 | 213 | ipfw('add', '1', 'check-state', 'ip', 214 | 'from', 'any', 'to', 'any') 215 | 216 | ipfw('add', '1', 'skipto', '2', 217 | 'tcp', 218 | 'from', 'any', 'to', 'table(125)') 219 | ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port, 220 | 'tcp', 221 | 'from', 'any', 'to', 'table(126)', 222 | 'not', 'ipttl', '42', 'keep-state', 'setup') 223 | 224 | ipfw_noexit('table', '124', 'flush') 225 | dnscount = 0 226 | for _, ip in [i for i in nslist if i[0] == family]: 227 | ipfw('table', '124', 'add', '%s' % (ip)) 228 | dnscount += 1 229 | if dnscount > 0: 230 | ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport, 231 | 'udp', 232 | 'from', 'any', 'to', 'table(124)', 233 | 'not', 'ipttl', '42') 234 | ipfw('add', '1', 'allow', 235 | 'udp', 236 | 'from', 'any', 'to', 'any', 237 | 'ipttl', '42') 238 | 239 | if subnets: 240 | # create new subnet entries 241 | for _, swidth, sexclude, snet \ 242 | in sorted(subnets, key=lambda s: s[1], reverse=True): 243 | if sexclude: 244 | ipfw('table', '125', 'add', '%s/%s' % (snet, swidth)) 245 | else: 246 | ipfw('table', '126', 'add', '%s/%s' % (snet, swidth)) 247 | 248 | def restore_firewall(self, port, family, udp, user): 249 | if family not in [socket.AF_INET]: 250 | raise Exception( 251 | 'Address family "%s" unsupported by tproxy method' 252 | % family_to_string(family)) 253 | 254 | ipfw_noexit('delete', '1') 255 | ipfw_noexit('table', '124', 'flush') 256 | ipfw_noexit('table', '125', 'flush') 257 | ipfw_noexit('table', '126', 'flush') 258 | -------------------------------------------------------------------------------- /sshuttle/methods/nat.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from sshuttle.firewall import subnet_weight 3 | from sshuttle.helpers import family_to_string 4 | from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal 5 | from sshuttle.methods import BaseMethod 6 | 7 | 8 | class Method(BaseMethod): 9 | 10 | # We name the chain based on the transproxy port number so that it's 11 | # possible to run multiple copies of sshuttle at the same time. Of course, 12 | # the multiple copies shouldn't have overlapping subnets, or only the most- 13 | # recently-started one will win (because we use "-I OUTPUT 1" instead of 14 | # "-A OUTPUT"). 15 | def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, 16 | user): 17 | # only ipv4 supported with NAT 18 | if family != socket.AF_INET: 19 | raise Exception( 20 | 'Address family "%s" unsupported by nat method_name' 21 | % family_to_string(family)) 22 | if udp: 23 | raise Exception("UDP not supported by nat method_name") 24 | 25 | table = "nat" 26 | 27 | def _ipt(*args): 28 | return ipt(family, table, *args) 29 | 30 | def _ipt_ttl(*args): 31 | return ipt_ttl(family, table, *args) 32 | 33 | def _ipm(*args): 34 | return ipt(family, "mangle", *args) 35 | 36 | chain = 'sshuttle-%s' % port 37 | 38 | # basic cleanup/setup of chains 39 | self.restore_firewall(port, family, udp, user) 40 | 41 | _ipt('-N', chain) 42 | _ipt('-F', chain) 43 | if user is not None: 44 | _ipm('-I', 'OUTPUT', '1', '-m', 'owner', '--uid-owner', str(user), 45 | '-j', 'MARK', '--set-mark', str(port)) 46 | args = '-m', 'mark', '--mark', str(port), '-j', chain 47 | else: 48 | args = '-j', chain 49 | 50 | _ipt('-I', 'OUTPUT', '1', *args) 51 | _ipt('-I', 'PREROUTING', '1', *args) 52 | 53 | # create new subnet entries. 54 | for _, swidth, sexclude, snet, fport, lport \ 55 | in sorted(subnets, key=subnet_weight, reverse=True): 56 | tcp_ports = ('-p', 'tcp') 57 | if fport: 58 | tcp_ports = tcp_ports + ('--dport', '%d:%d' % (fport, lport)) 59 | 60 | if sexclude: 61 | _ipt('-A', chain, '-j', 'RETURN', 62 | '--dest', '%s/%s' % (snet, swidth), 63 | *tcp_ports) 64 | else: 65 | _ipt_ttl('-A', chain, '-j', 'REDIRECT', 66 | '--dest', '%s/%s' % (snet, swidth), 67 | *(tcp_ports + ('--to-ports', str(port)))) 68 | 69 | for _, ip in [i for i in nslist if i[0] == family]: 70 | _ipt_ttl('-A', chain, '-j', 'REDIRECT', 71 | '--dest', '%s/32' % ip, 72 | '-p', 'udp', 73 | '--dport', '53', 74 | '--to-ports', str(dnsport)) 75 | 76 | def restore_firewall(self, port, family, udp, user): 77 | # only ipv4 supported with NAT 78 | if family != socket.AF_INET: 79 | raise Exception( 80 | 'Address family "%s" unsupported by nat method_name' 81 | % family_to_string(family)) 82 | if udp: 83 | raise Exception("UDP not supported by nat method_name") 84 | 85 | table = "nat" 86 | 87 | def _ipt(*args): 88 | return ipt(family, table, *args) 89 | 90 | def _ipt_ttl(*args): 91 | return ipt_ttl(family, table, *args) 92 | 93 | def _ipm(*args): 94 | return ipt(family, "mangle", *args) 95 | 96 | chain = 'sshuttle-%s' % port 97 | 98 | # basic cleanup/setup of chains 99 | if ipt_chain_exists(family, table, chain): 100 | if user is not None: 101 | nonfatal(_ipm, '-D', 'OUTPUT', '-m', 'owner', '--uid-owner', 102 | str(user), '-j', 'MARK', '--set-mark', str(port)) 103 | args = '-m', 'mark', '--mark', str(port), '-j', chain 104 | else: 105 | args = '-j', chain 106 | nonfatal(_ipt, '-D', 'OUTPUT', *args) 107 | nonfatal(_ipt, '-D', 'PREROUTING', *args) 108 | nonfatal(_ipt, '-F', chain) 109 | _ipt('-X', chain) 110 | 111 | def get_supported_features(self): 112 | result = super(Method, self).get_supported_features() 113 | result.user = True 114 | return result 115 | -------------------------------------------------------------------------------- /sshuttle/methods/nft.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from sshuttle.firewall import subnet_weight 3 | from sshuttle.linux import nft, nft_get_handle, nonfatal 4 | from sshuttle.methods import BaseMethod 5 | 6 | 7 | class Method(BaseMethod): 8 | 9 | # We name the chain based on the transproxy port number so that it's 10 | # possible to run multiple copies of sshuttle at the same time. Of course, 11 | # the multiple copies shouldn't have overlapping subnets, or only the most- 12 | # recently-started one will win (because we use "-I OUTPUT 1" instead of 13 | # "-A OUTPUT"). 14 | def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, 15 | user): 16 | if udp: 17 | raise Exception("UDP not supported by nft") 18 | 19 | table = "nat" 20 | 21 | def _nft(action, *args): 22 | return nft(family, table, action, *args) 23 | 24 | chain = 'sshuttle-%s' % port 25 | 26 | # basic cleanup/setup of chains 27 | _nft('add table', '') 28 | _nft('add chain', 'prerouting', 29 | '{ type nat hook prerouting priority -100; policy accept; }') 30 | _nft('add chain', 'postrouting', 31 | '{ type nat hook postrouting priority 100; policy accept; }') 32 | _nft('add chain', 'output', 33 | '{ type nat hook output priority -100; policy accept; }') 34 | _nft('add chain', chain) 35 | _nft('flush chain', chain) 36 | _nft('add rule', 'output jump %s' % chain) 37 | _nft('add rule', 'prerouting jump %s' % chain) 38 | 39 | # create new subnet entries. 40 | for _, swidth, sexclude, snet, fport, lport \ 41 | in sorted(subnets, key=subnet_weight, reverse=True): 42 | tcp_ports = ('ip', 'protocol', 'tcp') 43 | if fport and fport != lport: 44 | tcp_ports = \ 45 | tcp_ports + \ 46 | ('tcp', 'dport', '{ %d-%d }' % (fport, lport)) 47 | elif fport and fport == lport: 48 | tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport)) 49 | 50 | if sexclude: 51 | _nft('add rule', chain, *(tcp_ports + ( 52 | 'ip daddr %s/%s' % (snet, swidth), 'return'))) 53 | else: 54 | _nft('add rule', chain, *(tcp_ports + ( 55 | 'ip daddr %s/%s' % (snet, swidth), 'ip ttl != 42', 56 | ('redirect to :' + str(port))))) 57 | 58 | for _, ip in [i for i in nslist if i[0] == family]: 59 | if family == socket.AF_INET: 60 | _nft('add rule', chain, 'ip protocol udp ip daddr %s' % ip, 61 | 'udp dport { 53 }', 'ip ttl != 42', 62 | ('redirect to :' + str(dnsport))) 63 | elif family == socket.AF_INET6: 64 | _nft('add rule', chain, 'ip6 protocol udp ip6 daddr %s' % ip, 65 | 'udp dport { 53 }', 'ip ttl != 42', 66 | ('redirect to :' + str(dnsport))) 67 | 68 | def restore_firewall(self, port, family, udp, user): 69 | if udp: 70 | raise Exception("UDP not supported by nft method_name") 71 | 72 | table = "nat" 73 | 74 | def _nft(action, *args): 75 | return nft(family, table, action, *args) 76 | 77 | chain = 'sshuttle-%s' % port 78 | 79 | # basic cleanup/setup of chains 80 | handle = nft_get_handle('chain ip nat output', chain) 81 | nonfatal(_nft, 'delete rule', 'output', handle) 82 | handle = nft_get_handle('chain ip nat prerouting', chain) 83 | nonfatal(_nft, 'delete rule', 'prerouting', handle) 84 | nonfatal(_nft, 'delete chain', chain) 85 | -------------------------------------------------------------------------------- /sshuttle/methods/pf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import platform 4 | import re 5 | import socket 6 | import struct 7 | import subprocess as ssubprocess 8 | import shlex 9 | from fcntl import ioctl 10 | from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \ 11 | sizeof, addressof, memmove 12 | from sshuttle.firewall import subnet_weight 13 | from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string 14 | from sshuttle.methods import BaseMethod 15 | 16 | 17 | _pf_context = { 18 | 'started_by_sshuttle': 0, 19 | 'loaded_by_sshuttle': True, 20 | 'Xtoken': [] 21 | } 22 | _pf_fd = None 23 | 24 | 25 | class Generic(object): 26 | MAXPATHLEN = 1024 27 | PF_CHANGE_ADD_TAIL = 2 28 | PF_CHANGE_GET_TICKET = 6 29 | PF_PASS = 0 30 | PF_RDR = 8 31 | PF_OUT = 2 32 | ACTION_OFFSET = 0 33 | POOL_TICKET_OFFSET = 8 34 | ANCHOR_CALL_OFFSET = 1040 35 | 36 | class pf_addr(Structure): 37 | class _pfa(Union): 38 | _fields_ = [("v4", c_uint32), # struct in_addr 39 | ("v6", c_uint32 * 4), # struct in6_addr 40 | ("addr8", c_uint8 * 16), 41 | ("addr16", c_uint16 * 8), 42 | ("addr32", c_uint32 * 4)] 43 | 44 | _fields_ = [("pfa", _pfa)] 45 | _anonymous_ = ("pfa",) 46 | 47 | def __init__(self): 48 | self.status = b'' 49 | self.pfioc_pooladdr = c_char * 1136 50 | 51 | self.DIOCNATLOOK = ( 52 | (0x40000000 | 0x80000000) | 53 | ((sizeof(self.pfioc_natlook) & 0x1fff) << 16) | 54 | ((ord('D')) << 8) | (23)) 55 | self.DIOCCHANGERULE = ( 56 | (0x40000000 | 0x80000000) | 57 | ((sizeof(self.pfioc_rule) & 0x1fff) << 16) | 58 | ((ord('D')) << 8) | (26)) 59 | self.DIOCBEGINADDRS = ( 60 | (0x40000000 | 0x80000000) | 61 | ((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) | 62 | ((ord('D')) << 8) | (51)) 63 | 64 | def enable(self): 65 | if b'INFO:\nStatus: Disabled' in self.status: 66 | pfctl('-e') 67 | _pf_context['started_by_sshuttle'] += 1 68 | 69 | @staticmethod 70 | def disable(anchor): 71 | pfctl('-a %s -F all' % anchor) 72 | if _pf_context['started_by_sshuttle'] == 1: 73 | pfctl('-d') 74 | _pf_context['started_by_sshuttle'] -= 1 75 | 76 | def query_nat(self, family, proto, src_ip, src_port, dst_ip, dst_port): 77 | [proto, family, src_port, dst_port] = [ 78 | int(v) for v in [proto, family, src_port, dst_port]] 79 | 80 | packed_src_ip = socket.inet_pton(family, src_ip) 81 | packed_dst_ip = socket.inet_pton(family, dst_ip) 82 | 83 | assert len(packed_src_ip) == len(packed_dst_ip) 84 | length = len(packed_src_ip) 85 | 86 | pnl = self.pfioc_natlook() 87 | pnl.proto = proto 88 | pnl.direction = self.PF_OUT 89 | pnl.af = family 90 | memmove(addressof(pnl.saddr), packed_src_ip, length) 91 | memmove(addressof(pnl.daddr), packed_dst_ip, length) 92 | self._add_natlook_ports(pnl, src_port, dst_port) 93 | 94 | ioctl(pf_get_dev(), self.DIOCNATLOOK, 95 | (c_char * sizeof(pnl)).from_address(addressof(pnl))) 96 | 97 | ip = socket.inet_ntop( 98 | pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)).raw) 99 | port = socket.ntohs(self._get_natlook_port(pnl.rdxport)) 100 | return (ip, port) 101 | 102 | @staticmethod 103 | def _add_natlook_ports(pnl, src_port, dst_port): 104 | pnl.sxport = socket.htons(src_port) 105 | pnl.dxport = socket.htons(dst_port) 106 | 107 | @staticmethod 108 | def _get_natlook_port(xport): 109 | return xport 110 | 111 | def add_anchors(self, anchor, status=None): 112 | if status is None: 113 | status = pfctl('-s all')[0] 114 | self.status = status 115 | if ('\nanchor "%s"' % anchor).encode('ASCII') not in status: 116 | self._add_anchor_rule(self.PF_PASS, anchor.encode('ASCII')) 117 | 118 | def _add_anchor_rule(self, kind, name, pr=None): 119 | if pr is None: 120 | pr = self.pfioc_rule() 121 | 122 | memmove(addressof(pr) + self.ANCHOR_CALL_OFFSET, name, 123 | min(self.MAXPATHLEN, len(name))) # anchor_call = name 124 | memmove(addressof(pr) + self.RULE_ACTION_OFFSET, 125 | struct.pack('I', kind), 4) # rule.action = kind 126 | 127 | memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack( 128 | 'I', self.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET 129 | ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) 130 | 131 | memmove(addressof(pr) + self.ACTION_OFFSET, struct.pack( 132 | 'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL 133 | ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) 134 | 135 | @staticmethod 136 | def _inet_version(family): 137 | return b'inet' if family == socket.AF_INET else b'inet6' 138 | 139 | @staticmethod 140 | def _lo_addr(family): 141 | return b'127.0.0.1' if family == socket.AF_INET else b'::1' 142 | 143 | @staticmethod 144 | def add_rules(anchor, rules): 145 | assert isinstance(rules, bytes) 146 | debug3("rules:\n" + rules.decode("ASCII")) 147 | pfctl('-a %s -f /dev/stdin' % anchor, rules) 148 | 149 | @staticmethod 150 | def has_skip_loopback(): 151 | return b'skip' in pfctl('-s Interfaces -i lo -v')[0] 152 | 153 | 154 | 155 | class FreeBsd(Generic): 156 | RULE_ACTION_OFFSET = 2968 157 | 158 | def __new__(cls): 159 | class pfioc_natlook(Structure): 160 | pf_addr = Generic.pf_addr 161 | _fields_ = [("saddr", pf_addr), 162 | ("daddr", pf_addr), 163 | ("rsaddr", pf_addr), 164 | ("rdaddr", pf_addr), 165 | ("sxport", c_uint16), 166 | ("dxport", c_uint16), 167 | ("rsxport", c_uint16), 168 | ("rdxport", c_uint16), 169 | ("af", c_uint8), # sa_family_t 170 | ("proto", c_uint8), 171 | ("proto_variant", c_uint8), 172 | ("direction", c_uint8)] 173 | 174 | freebsd = Generic.__new__(cls) 175 | freebsd.pfioc_rule = c_char * 3040 176 | freebsd.pfioc_natlook = pfioc_natlook 177 | return freebsd 178 | 179 | def enable(self): 180 | returncode = ssubprocess.call(['kldload', 'pf']) 181 | super(FreeBsd, self).enable() 182 | if returncode == 0: 183 | _pf_context['loaded_by_sshuttle'] = True 184 | 185 | def disable(self, anchor): 186 | super(FreeBsd, self).disable(anchor) 187 | if _pf_context['loaded_by_sshuttle'] and \ 188 | _pf_context['started_by_sshuttle'] == 0: 189 | ssubprocess.call(['kldunload', 'pf']) 190 | 191 | def add_anchors(self, anchor): 192 | status = pfctl('-s all')[0] 193 | if ('\nrdr-anchor "%s"' % anchor).encode('ASCII') not in status: 194 | self._add_anchor_rule(self.PF_RDR, anchor.encode('ASCII')) 195 | super(FreeBsd, self).add_anchors(anchor, status=status) 196 | 197 | def _add_anchor_rule(self, kind, name, pr=None): 198 | pr = pr or self.pfioc_rule() 199 | ppa = self.pfioc_pooladdr() 200 | 201 | ioctl(pf_get_dev(), self.DIOCBEGINADDRS, ppa) 202 | # pool ticket 203 | memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4) 204 | super(FreeBsd, self)._add_anchor_rule(kind, name, pr=pr) 205 | 206 | def add_rules(self, anchor, includes, port, dnsport, nslist, family): 207 | inet_version = self._inet_version(family) 208 | lo_addr = self._lo_addr(family) 209 | 210 | tables = [] 211 | translating_rules = [ 212 | b'rdr pass on lo0 %s proto tcp from ! %s to %s ' 213 | b'-> %s port %r' % (inet_version, lo_addr, subnet, lo_addr, port) 214 | for exclude, subnet in includes if not exclude 215 | ] 216 | filtering_rules = [ 217 | b'pass out route-to lo0 %s proto tcp ' 218 | b'to %s keep state' % (inet_version, subnet) 219 | if not exclude else 220 | b'pass out quick %s proto tcp to %s' % (inet_version, subnet) 221 | for exclude, subnet in includes 222 | ] 223 | 224 | if nslist: 225 | tables.append( 226 | b'table {%s}' % 227 | b','.join([ns[1].encode("ASCII") for ns in nslist])) 228 | translating_rules.append( 229 | b'rdr pass on lo0 %s proto udp to ' 230 | b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport)) 231 | filtering_rules.append( 232 | b'pass out route-to lo0 %s proto udp to ' 233 | b' port 53 keep state' % inet_version) 234 | 235 | rules = b'\n'.join(tables + translating_rules + filtering_rules) \ 236 | + b'\n' 237 | 238 | super(FreeBsd, self).add_rules(anchor, rules) 239 | 240 | 241 | class OpenBsd(Generic): 242 | POOL_TICKET_OFFSET = 4 243 | RULE_ACTION_OFFSET = 3324 244 | ANCHOR_CALL_OFFSET = 1036 245 | 246 | def __init__(self): 247 | class pfioc_natlook(Structure): 248 | pf_addr = Generic.pf_addr 249 | _fields_ = [("saddr", pf_addr), 250 | ("daddr", pf_addr), 251 | ("rsaddr", pf_addr), 252 | ("rdaddr", pf_addr), 253 | ("rdomain", c_uint16), 254 | ("rrdomain", c_uint16), 255 | ("sxport", c_uint16), 256 | ("dxport", c_uint16), 257 | ("rsxport", c_uint16), 258 | ("rdxport", c_uint16), 259 | ("af", c_uint8), # sa_family_t 260 | ("proto", c_uint8), 261 | ("proto_variant", c_uint8), 262 | ("direction", c_uint8)] 263 | 264 | self.pfioc_rule = c_char * 3400 265 | self.pfioc_natlook = pfioc_natlook 266 | super(OpenBsd, self).__init__() 267 | 268 | def add_anchors(self, anchor): 269 | # before adding anchors and rules we must override the skip lo 270 | # that comes by default in openbsd pf.conf so the rules we will add, 271 | # which rely on translating/filtering packets on lo, can work 272 | if self.has_skip_loopback(): 273 | pfctl('-f /dev/stdin', b'match on lo\n') 274 | super(OpenBsd, self).add_anchors(anchor) 275 | 276 | def add_rules(self, anchor, includes, port, dnsport, nslist, family): 277 | inet_version = self._inet_version(family) 278 | lo_addr = self._lo_addr(family) 279 | 280 | tables = [] 281 | translating_rules = [ 282 | b'pass in on lo0 %s proto tcp to %s ' 283 | b'divert-to %s port %r' % (inet_version, subnet, lo_addr, port) 284 | for exclude, subnet in includes if not exclude 285 | ] 286 | filtering_rules = [ 287 | b'pass out %s proto tcp to %s ' 288 | b'route-to lo0 keep state' % (inet_version, subnet) 289 | if not exclude else 290 | b'pass out quick %s proto tcp to %s' % (inet_version, subnet) 291 | for exclude, subnet in includes 292 | ] 293 | 294 | if nslist: 295 | tables.append( 296 | b'table {%s}' % 297 | b','.join([ns[1].encode("ASCII") for ns in nslist])) 298 | translating_rules.append( 299 | b'pass in on lo0 %s proto udp to port 53 ' 300 | b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport)) 301 | filtering_rules.append( 302 | b'pass out %s proto udp to port 53 ' 303 | b'route-to lo0 keep state' % inet_version) 304 | 305 | rules = b'\n'.join(tables + translating_rules + filtering_rules) \ 306 | + b'\n' 307 | 308 | super(OpenBsd, self).add_rules(anchor, rules) 309 | 310 | 311 | class Darwin(FreeBsd): 312 | RULE_ACTION_OFFSET = 3068 313 | 314 | def __init__(self): 315 | class pf_state_xport(Union): 316 | _fields_ = [("port", c_uint16), 317 | ("call_id", c_uint16), 318 | ("spi", c_uint32)] 319 | 320 | class pfioc_natlook(Structure): 321 | pf_addr = Generic.pf_addr 322 | _fields_ = [("saddr", pf_addr), 323 | ("daddr", pf_addr), 324 | ("rsaddr", pf_addr), 325 | ("rdaddr", pf_addr), 326 | ("sxport", pf_state_xport), 327 | ("dxport", pf_state_xport), 328 | ("rsxport", pf_state_xport), 329 | ("rdxport", pf_state_xport), 330 | ("af", c_uint8), # sa_family_t 331 | ("proto", c_uint8), 332 | ("proto_variant", c_uint8), 333 | ("direction", c_uint8)] 334 | 335 | self.pfioc_rule = c_char * 3104 336 | self.pfioc_natlook = pfioc_natlook 337 | super(Darwin, self).__init__() 338 | 339 | def enable(self): 340 | o = pfctl('-E') 341 | _pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1)) 342 | 343 | def disable(self, anchor): 344 | pfctl('-a %s -F all' % anchor) 345 | if _pf_context['Xtoken']: 346 | pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII")) 347 | 348 | def add_anchors(self, anchor): 349 | # before adding anchors and rules we must override the skip lo 350 | # that in some cases ends up in the chain so the rules we will add, 351 | # which rely on translating/filtering packets on lo, can work 352 | if self.has_skip_loopback(): 353 | pfctl('-f /dev/stdin', b'pass on lo\n') 354 | super(Darwin, self).add_anchors(anchor) 355 | 356 | def _add_natlook_ports(self, pnl, src_port, dst_port): 357 | pnl.sxport.port = socket.htons(src_port) 358 | pnl.dxport.port = socket.htons(dst_port) 359 | 360 | def _get_natlook_port(self, xport): 361 | return xport.port 362 | 363 | 364 | class PfSense(FreeBsd): 365 | RULE_ACTION_OFFSET = 3040 366 | 367 | def __init__(self): 368 | self.pfioc_rule = c_char * 3112 369 | super(PfSense, self).__init__() 370 | 371 | 372 | if sys.platform == 'darwin': 373 | pf = Darwin() 374 | elif sys.platform.startswith('openbsd'): 375 | pf = OpenBsd() 376 | elif platform.version().endswith('pfSense'): 377 | pf = PfSense() 378 | else: 379 | pf = FreeBsd() 380 | 381 | 382 | def pfctl(args, stdin=None): 383 | argv = ['pfctl'] + shlex.split(args) 384 | debug1('>> %s\n' % ' '.join(argv)) 385 | 386 | env = { 387 | 'PATH': os.environ['PATH'], 388 | 'LC_ALL': "C", 389 | } 390 | p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE, 391 | stdout=ssubprocess.PIPE, 392 | stderr=ssubprocess.PIPE, 393 | env=env) 394 | o = p.communicate(stdin) 395 | if p.returncode: 396 | raise Fatal('%r returned %d' % (argv, p.returncode)) 397 | 398 | return o 399 | 400 | 401 | def pf_get_dev(): 402 | global _pf_fd 403 | if _pf_fd is None: 404 | _pf_fd = os.open('/dev/pf', os.O_RDWR) 405 | 406 | return _pf_fd 407 | 408 | 409 | def pf_get_anchor(family, port): 410 | return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port) 411 | 412 | 413 | class Method(BaseMethod): 414 | 415 | def get_supported_features(self): 416 | result = super(Method, self).get_supported_features() 417 | result.ipv6 = True 418 | return result 419 | 420 | def get_tcp_dstip(self, sock): 421 | pfile = self.firewall.pfile 422 | 423 | peer = sock.getpeername() 424 | proxy = sock.getsockname() 425 | 426 | argv = (sock.family, socket.IPPROTO_TCP, 427 | peer[0].encode("ASCII"), peer[1], 428 | proxy[0].encode("ASCII"), proxy[1]) 429 | out_line = b"QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv 430 | pfile.write(out_line) 431 | pfile.flush() 432 | in_line = pfile.readline() 433 | debug2(out_line.decode("ASCII") + ' > ' + in_line.decode("ASCII")) 434 | if in_line.startswith(b'QUERY_PF_NAT_SUCCESS '): 435 | (ip, port) = in_line[21:].split(b',') 436 | return (ip.decode("ASCII"), int(port)) 437 | 438 | return sock.getsockname() 439 | 440 | def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, 441 | user): 442 | if family not in [socket.AF_INET, socket.AF_INET6]: 443 | raise Exception( 444 | 'Address family "%s" unsupported by pf method_name' 445 | % family_to_string(family)) 446 | if udp: 447 | raise Exception("UDP not supported by pf method_name") 448 | 449 | if subnets: 450 | includes = [] 451 | # If a given subnet is both included and excluded, list the 452 | # exclusion first; the table will ignore the second, opposite 453 | # definition 454 | for _, swidth, sexclude, snet, fport, lport \ 455 | in sorted(subnets, key=subnet_weight, reverse=True): 456 | includes.append((sexclude, b"%s/%d%s" % ( 457 | snet.encode("ASCII"), 458 | swidth, 459 | b" port %d:%d" % (fport, lport) if fport else b""))) 460 | 461 | anchor = pf_get_anchor(family, port) 462 | pf.add_anchors(anchor) 463 | pf.add_rules(anchor, includes, port, dnsport, nslist, family) 464 | pf.enable() 465 | 466 | def restore_firewall(self, port, family, udp, user): 467 | if family not in [socket.AF_INET, socket.AF_INET6]: 468 | raise Exception( 469 | 'Address family "%s" unsupported by pf method_name' 470 | % family_to_string(family)) 471 | if udp: 472 | raise Exception("UDP not supported by pf method_name") 473 | 474 | pf.disable(pf_get_anchor(family, port)) 475 | 476 | def firewall_command(self, line): 477 | if line.startswith('QUERY_PF_NAT '): 478 | try: 479 | dst = pf.query_nat(*(line[13:].split(','))) 480 | sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst) 481 | except IOError as e: 482 | sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e) 483 | 484 | sys.stdout.flush() 485 | return True 486 | else: 487 | return False 488 | -------------------------------------------------------------------------------- /sshuttle/methods/tproxy.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from sshuttle.firewall import subnet_weight 3 | from sshuttle.helpers import family_to_string 4 | from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists 5 | from sshuttle.methods import BaseMethod 6 | from sshuttle.helpers import debug1, debug3, Fatal 7 | 8 | recvmsg = None 9 | try: 10 | # try getting recvmsg from python 11 | import socket as pythonsocket 12 | getattr(pythonsocket.socket, "recvmsg") 13 | socket = pythonsocket 14 | recvmsg = "python" 15 | except AttributeError: 16 | # try getting recvmsg from socket_ext library 17 | try: 18 | import socket_ext 19 | getattr(socket_ext.socket, "recvmsg") 20 | socket = socket_ext 21 | recvmsg = "socket_ext" 22 | except ImportError: 23 | import socket 24 | 25 | 26 | IP_TRANSPARENT = 19 27 | IP_ORIGDSTADDR = 20 28 | IP_RECVORIGDSTADDR = IP_ORIGDSTADDR 29 | SOL_IPV6 = 41 30 | IPV6_ORIGDSTADDR = 74 31 | IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR 32 | 33 | if recvmsg == "python": 34 | def recv_udp(listener, bufsize): 35 | debug3('Accept UDP python using recvmsg.\n') 36 | data, ancdata, _, srcip = listener.recvmsg( 37 | 4096, socket.CMSG_SPACE(24)) 38 | dstip = None 39 | family = None 40 | for cmsg_level, cmsg_type, cmsg_data in ancdata: 41 | if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR: 42 | family, port = struct.unpack('=HH', cmsg_data[0:4]) 43 | port = socket.htons(port) 44 | if family == socket.AF_INET: 45 | start = 4 46 | length = 4 47 | else: 48 | raise Fatal("Unsupported socket type '%s'" % family) 49 | ip = socket.inet_ntop(family, cmsg_data[start:start + length]) 50 | dstip = (ip, port) 51 | break 52 | elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR: 53 | family, port = struct.unpack('=HH', cmsg_data[0:4]) 54 | port = socket.htons(port) 55 | if family == socket.AF_INET6: 56 | start = 8 57 | length = 16 58 | else: 59 | raise Fatal("Unsupported socket type '%s'" % family) 60 | ip = socket.inet_ntop(family, cmsg_data[start:start + length]) 61 | dstip = (ip, port) 62 | break 63 | return (srcip, dstip, data) 64 | elif recvmsg == "socket_ext": 65 | def recv_udp(listener, bufsize): 66 | debug3('Accept UDP using socket_ext recvmsg.\n') 67 | srcip, data, adata, _ = listener.recvmsg( 68 | (bufsize,), socket.CMSG_SPACE(24)) 69 | dstip = None 70 | family = None 71 | for a in adata: 72 | if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: 73 | family, port = struct.unpack('=HH', a.cmsg_data[0:4]) 74 | port = socket.htons(port) 75 | if family == socket.AF_INET: 76 | start = 4 77 | length = 4 78 | else: 79 | raise Fatal("Unsupported socket type '%s'" % family) 80 | ip = socket.inet_ntop( 81 | family, a.cmsg_data[start:start + length]) 82 | dstip = (ip, port) 83 | break 84 | elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR: 85 | family, port = struct.unpack('=HH', a.cmsg_data[0:4]) 86 | port = socket.htons(port) 87 | if family == socket.AF_INET6: 88 | start = 8 89 | length = 16 90 | else: 91 | raise Fatal("Unsupported socket type '%s'" % family) 92 | ip = socket.inet_ntop( 93 | family, a.cmsg_data[start:start + length]) 94 | dstip = (ip, port) 95 | break 96 | return (srcip, dstip, data[0]) 97 | else: 98 | def recv_udp(listener, bufsize): 99 | debug3('Accept UDP using recvfrom.\n') 100 | data, srcip = listener.recvfrom(bufsize) 101 | return (srcip, None, data) 102 | 103 | 104 | class Method(BaseMethod): 105 | 106 | def get_supported_features(self): 107 | result = super(Method, self).get_supported_features() 108 | result.ipv6 = True 109 | if recvmsg is None: 110 | result.udp = False 111 | result.dns = False 112 | else: 113 | result.udp = True 114 | result.dns = True 115 | return result 116 | 117 | def get_tcp_dstip(self, sock): 118 | return sock.getsockname() 119 | 120 | def recv_udp(self, udp_listener, bufsize): 121 | srcip, dstip, data = recv_udp(udp_listener, bufsize) 122 | if not dstip: 123 | debug1( 124 | "-- ignored UDP from %r: " 125 | "couldn't determine destination IP address\n" % (srcip,)) 126 | return None 127 | return srcip, dstip, data 128 | 129 | def send_udp(self, sock, srcip, dstip, data): 130 | if not srcip: 131 | debug1( 132 | "-- ignored UDP to %r: " 133 | "couldn't determine source IP address\n" % (dstip,)) 134 | return 135 | sender = socket.socket(sock.family, socket.SOCK_DGRAM) 136 | sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 137 | sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 138 | sender.bind(srcip) 139 | sender.sendto(data, dstip) 140 | sender.close() 141 | 142 | def setup_tcp_listener(self, tcp_listener): 143 | tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 144 | 145 | def setup_udp_listener(self, udp_listener): 146 | udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 147 | if udp_listener.v4 is not None: 148 | udp_listener.v4.setsockopt( 149 | socket.SOL_IP, IP_RECVORIGDSTADDR, 1) 150 | if udp_listener.v6 is not None: 151 | udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) 152 | 153 | def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, 154 | user): 155 | if family not in [socket.AF_INET, socket.AF_INET6]: 156 | raise Exception( 157 | 'Address family "%s" unsupported by tproxy method' 158 | % family_to_string(family)) 159 | 160 | table = "mangle" 161 | 162 | def _ipt(*args): 163 | return ipt(family, table, *args) 164 | 165 | def _ipt_ttl(*args): 166 | return ipt_ttl(family, table, *args) 167 | 168 | def _ipt_proto_ports(proto, fport, lport): 169 | return proto + ('--dport', '%d:%d' % (fport, lport)) \ 170 | if fport else proto 171 | 172 | 173 | mark_chain = 'sshuttle-m-%s' % port 174 | tproxy_chain = 'sshuttle-t-%s' % port 175 | divert_chain = 'sshuttle-d-%s' % port 176 | 177 | # basic cleanup/setup of chains 178 | self.restore_firewall(port, family, udp, user) 179 | 180 | _ipt('-N', mark_chain) 181 | _ipt('-F', mark_chain) 182 | _ipt('-N', divert_chain) 183 | _ipt('-F', divert_chain) 184 | _ipt('-N', tproxy_chain) 185 | _ipt('-F', tproxy_chain) 186 | _ipt('-I', 'OUTPUT', '1', '-j', mark_chain) 187 | _ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain) 188 | _ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1') 189 | _ipt('-A', divert_chain, '-j', 'ACCEPT') 190 | _ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, 191 | '-m', 'tcp', '-p', 'tcp') 192 | 193 | if udp: 194 | _ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, 195 | '-m', 'udp', '-p', 'udp') 196 | 197 | for _, ip in [i for i in nslist if i[0] == family]: 198 | _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', 199 | '--dest', '%s/32' % ip, 200 | '-m', 'udp', '-p', 'udp', '--dport', '53') 201 | _ipt('-A', tproxy_chain, '-j', 'TPROXY', 202 | '--tproxy-mark', '0x1/0x1', 203 | '--dest', '%s/32' % ip, 204 | '-m', 'udp', '-p', 'udp', '--dport', '53', 205 | '--on-port', str(dnsport)) 206 | 207 | for _, swidth, sexclude, snet, fport, lport \ 208 | in sorted(subnets, key=subnet_weight, reverse=True): 209 | tcp_ports = ('-p', 'tcp') 210 | tcp_ports = _ipt_proto_ports(tcp_ports, fport, lport) 211 | 212 | if sexclude: 213 | _ipt('-A', mark_chain, '-j', 'RETURN', 214 | '--dest', '%s/%s' % (snet, swidth), 215 | '-m', 'tcp', 216 | *tcp_ports) 217 | _ipt('-A', tproxy_chain, '-j', 'RETURN', 218 | '--dest', '%s/%s' % (snet, swidth), 219 | '-m', 'tcp', 220 | *tcp_ports) 221 | else: 222 | _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', 223 | '--dest', '%s/%s' % (snet, swidth), 224 | '-m', 'tcp', 225 | *tcp_ports) 226 | _ipt('-A', tproxy_chain, '-j', 'TPROXY', 227 | '--tproxy-mark', '0x1/0x1', 228 | '--dest', '%s/%s' % (snet, swidth), 229 | '-m', 'tcp', 230 | *(tcp_ports + ('--on-port', str(port)))) 231 | 232 | if udp: 233 | udp_ports = ('-p', 'udp') 234 | udp_ports = _ipt_proto_ports(udp_ports, fport, lport) 235 | 236 | if sexclude: 237 | _ipt('-A', mark_chain, '-j', 'RETURN', 238 | '--dest', '%s/%s' % (snet, swidth), 239 | '-m', 'udp', 240 | *udp_ports) 241 | _ipt('-A', tproxy_chain, '-j', 'RETURN', 242 | '--dest', '%s/%s' % (snet, swidth), 243 | '-m', 'udp', 244 | *udp_ports) 245 | else: 246 | _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', 247 | '--dest', '%s/%s' % (snet, swidth), 248 | '-m', 'udp', '-p', 'udp') 249 | _ipt('-A', tproxy_chain, '-j', 'TPROXY', 250 | '--tproxy-mark', '0x1/0x1', 251 | '--dest', '%s/%s' % (snet, swidth), 252 | '-m', 'udp', 253 | *(udp_ports + ('--on-port', str(port)))) 254 | 255 | def restore_firewall(self, port, family, udp, user): 256 | if family not in [socket.AF_INET, socket.AF_INET6]: 257 | raise Exception( 258 | 'Address family "%s" unsupported by tproxy method' 259 | % family_to_string(family)) 260 | 261 | table = "mangle" 262 | 263 | def _ipt(*args): 264 | return ipt(family, table, *args) 265 | 266 | def _ipt_ttl(*args): 267 | return ipt_ttl(family, table, *args) 268 | 269 | mark_chain = 'sshuttle-m-%s' % port 270 | tproxy_chain = 'sshuttle-t-%s' % port 271 | divert_chain = 'sshuttle-d-%s' % port 272 | 273 | # basic cleanup/setup of chains 274 | if ipt_chain_exists(family, table, mark_chain): 275 | _ipt('-D', 'OUTPUT', '-j', mark_chain) 276 | _ipt('-F', mark_chain) 277 | _ipt('-X', mark_chain) 278 | 279 | if ipt_chain_exists(family, table, tproxy_chain): 280 | _ipt('-D', 'PREROUTING', '-j', tproxy_chain) 281 | _ipt('-F', tproxy_chain) 282 | _ipt('-X', tproxy_chain) 283 | 284 | if ipt_chain_exists(family, table, divert_chain): 285 | _ipt('-F', divert_chain) 286 | _ipt('-X', divert_chain) 287 | -------------------------------------------------------------------------------- /sshuttle/options.py: -------------------------------------------------------------------------------- 1 | import re 2 | import socket 3 | from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal 4 | from sshuttle import __version__ 5 | 6 | 7 | # Subnet file, supporting empty lines and hash-started comment lines 8 | def parse_subnetport_file(s): 9 | try: 10 | handle = open(s, 'r') 11 | except OSError: 12 | raise Fatal('Unable to open subnet file: %s' % s) 13 | 14 | raw_config_lines = handle.readlines() 15 | subnets = [] 16 | for _, line in enumerate(raw_config_lines): 17 | line = line.strip() 18 | if not line: 19 | continue 20 | if line[0] == '#': 21 | continue 22 | subnets.append(parse_subnetport(line)) 23 | 24 | return subnets 25 | 26 | 27 | # 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4 28 | # [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3 29 | # example.com:123 or just example.com 30 | def parse_subnetport(s): 31 | if s.count(':') > 1: 32 | rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$' 33 | else: 34 | rx = r'([\w\.]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$' 35 | 36 | m = re.match(rx, s) 37 | if not m: 38 | raise Fatal('%r is not a valid address/mask:port format' % s) 39 | 40 | addr, width, fport, lport = m.groups() 41 | try: 42 | addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM) 43 | except socket.gaierror: 44 | raise Fatal('Unable to resolve address: %s' % addr) 45 | 46 | family, _, _, _, addr = min(addrinfo) 47 | max_width = 32 if family == socket.AF_INET else 128 48 | width = int(width or max_width) 49 | if not 0 <= width <= max_width: 50 | raise Fatal('width %d is not between 0 and %d' % (width, max_width)) 51 | 52 | return (family, addr[0], width, int(fport or 0), int(lport or fport or 0)) 53 | 54 | 55 | # 1.2.3.4:567 or just 1.2.3.4 or just 567 56 | # [1:2::3]:456 or [1:2::3] or just [::]:567 57 | # example.com:123 or just example.com 58 | def parse_ipport(s): 59 | s = str(s) 60 | if s.isdigit(): 61 | rx = r'()(\d+)$' 62 | elif ']' in s: 63 | rx = r'(?:\[([^]]+)])(?::(\d+))?$' 64 | else: 65 | rx = r'([\w\.]+)(?::(\d+))?$' 66 | 67 | m = re.match(rx, s) 68 | if not m: 69 | raise Fatal('%r is not a valid IP:port format' % s) 70 | 71 | ip, port = m.groups() 72 | ip = ip or '0.0.0.0' 73 | port = int(port or 0) 74 | 75 | try: 76 | addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM) 77 | except socket.gaierror: 78 | raise Fatal('%r is not a valid IP:port format' % s) 79 | 80 | family, _, _, _, addr = min(addrinfo) 81 | return (family,) + addr[:2] 82 | 83 | 84 | def parse_list(lst): 85 | return re.split(r'[\s,]+', lst.strip()) if lst else [] 86 | 87 | 88 | class Concat(Action): 89 | def __init__(self, option_strings, dest, nargs=None, **kwargs): 90 | if nargs is not None: 91 | raise ValueError("nargs not supported") 92 | super(Concat, self).__init__(option_strings, dest, **kwargs) 93 | 94 | def __call__(self, parser, namespace, values, option_string=None): 95 | curr_value = getattr(namespace, self.dest, None) or [] 96 | setattr(namespace, self.dest, curr_value + values) 97 | 98 | 99 | parser = ArgumentParser( 100 | prog="sshuttle", 101 | usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] ", 102 | fromfile_prefix_chars="@" 103 | ) 104 | parser.add_argument( 105 | "subnets", 106 | metavar="IP/MASK[:PORT[-PORT]]...", 107 | nargs="*", 108 | type=parse_subnetport, 109 | help=""" 110 | capture and forward traffic to these subnets (whitespace separated) 111 | """ 112 | ) 113 | parser.add_argument( 114 | "-l", "--listen", 115 | metavar="[IP:]PORT", 116 | help=""" 117 | transproxy to this ip address and port number 118 | """ 119 | ) 120 | parser.add_argument( 121 | "-H", "--auto-hosts", 122 | action="store_true", 123 | help=""" 124 | continuously scan for remote hostnames and update local /etc/hosts as 125 | they are found 126 | """ 127 | ) 128 | parser.add_argument( 129 | "-N", "--auto-nets", 130 | action="store_true", 131 | help=""" 132 | automatically determine subnets to route 133 | """ 134 | ) 135 | parser.add_argument( 136 | "--dns", 137 | action="store_true", 138 | help=""" 139 | capture local DNS requests and forward to the remote DNS server 140 | """ 141 | ) 142 | parser.add_argument( 143 | "--ns-hosts", 144 | metavar="IP[,IP]", 145 | default=[], 146 | type=parse_list, 147 | help=""" 148 | capture and forward DNS requests made to the following servers 149 | """ 150 | ) 151 | parser.add_argument( 152 | "--to-ns", 153 | metavar="IP[:PORT]", 154 | type=parse_ipport, 155 | help=""" 156 | the DNS server to forward requests to; defaults to servers in 157 | /etc/resolv.conf on remote side if not given. 158 | """ 159 | ) 160 | 161 | parser.add_argument( 162 | "--method", 163 | choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"], 164 | metavar="TYPE", 165 | default="auto", 166 | help=""" 167 | %(choices)s 168 | """ 169 | ) 170 | parser.add_argument( 171 | "--python", 172 | metavar="PATH", 173 | help=""" 174 | path to python interpreter on the remote server 175 | """ 176 | ) 177 | parser.add_argument( 178 | "-r", "--remote", 179 | metavar="[USERNAME@]ADDR[:PORT]", 180 | help=""" 181 | ssh hostname (and optional username) of remote %(prog)s server 182 | """ 183 | ) 184 | parser.add_argument( 185 | "-x", "--exclude", 186 | metavar="IP/MASK[:PORT[-PORT]]", 187 | action="append", 188 | default=[], 189 | type=parse_subnetport, 190 | help=""" 191 | exclude this subnet (can be used more than once) 192 | """ 193 | ) 194 | parser.add_argument( 195 | "-X", "--exclude-from", 196 | metavar="PATH", 197 | action=Concat, 198 | dest="exclude", 199 | type=parse_subnetport_file, 200 | help=""" 201 | exclude the subnets in a file (whitespace separated) 202 | """ 203 | ) 204 | parser.add_argument( 205 | "-v", "--verbose", 206 | action="count", 207 | default=0, 208 | help=""" 209 | increase debug message verbosity 210 | """ 211 | ) 212 | parser.add_argument( 213 | "-V", "--version", 214 | action="version", 215 | version=__version__, 216 | help=""" 217 | print the %(prog)s version number and exit 218 | """ 219 | ) 220 | parser.add_argument( 221 | "-e", "--ssh-cmd", 222 | metavar="CMD", 223 | default="ssh", 224 | help=""" 225 | the command to use to connect to the remote [%(default)s] 226 | """ 227 | ) 228 | parser.add_argument( 229 | "--seed-hosts", 230 | metavar="HOSTNAME[,HOSTNAME]", 231 | default=[], 232 | help=""" 233 | comma-separated list of hostnames for initial scan (may be used with 234 | or without --auto-hosts) 235 | """ 236 | ) 237 | parser.add_argument( 238 | "--no-latency-control", 239 | action="store_false", 240 | dest="latency_control", 241 | help=""" 242 | sacrifice latency to improve bandwidth benchmarks 243 | """ 244 | ) 245 | parser.add_argument( 246 | "--wrap", 247 | metavar="NUM", 248 | type=int, 249 | help=""" 250 | restart counting channel numbers after this number (for testing) 251 | """ 252 | ) 253 | parser.add_argument( 254 | "--disable-ipv6", 255 | action="store_true", 256 | help=""" 257 | disable IPv6 support 258 | """ 259 | ) 260 | parser.add_argument( 261 | "-D", "--daemon", 262 | action="store_true", 263 | help=""" 264 | run in the background as a daemon 265 | """ 266 | ) 267 | parser.add_argument( 268 | "-s", "--subnets", 269 | metavar="PATH", 270 | action=Concat, 271 | dest="subnets_file", 272 | default=[], 273 | type=parse_subnetport_file, 274 | help=""" 275 | file where the subnets are stored, instead of on the command line 276 | """ 277 | ) 278 | parser.add_argument( 279 | "--syslog", 280 | action="store_true", 281 | help=""" 282 | send log messages to syslog (default if you use --daemon) 283 | """ 284 | ) 285 | parser.add_argument( 286 | "--pidfile", 287 | metavar="PATH", 288 | default="./sshuttle.pid", 289 | help=""" 290 | pidfile name (only if using --daemon) [%(default)s] 291 | """ 292 | ) 293 | parser.add_argument( 294 | "--user", 295 | help=""" 296 | apply all the rules only to this linux user 297 | """ 298 | ) 299 | parser.add_argument( 300 | "--firewall", 301 | action="store_true", 302 | help=""" 303 | (internal use only) 304 | """ 305 | ) 306 | parser.add_argument( 307 | "--hostwatch", 308 | action="store_true", 309 | help=""" 310 | (internal use only) 311 | """ 312 | ) 313 | -------------------------------------------------------------------------------- /sshuttle/sdnotify.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import os 3 | from sshuttle.helpers import debug1 4 | 5 | def _notify(message): 6 | addr = os.environ.get("NOTIFY_SOCKET", None) 7 | 8 | if not addr or len(addr) == 1 or addr[0] not in ('/', '@'): 9 | return False 10 | 11 | addr = '\0' + addr[1:] if addr[0] == '@' else addr 12 | 13 | try: 14 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 15 | except (OSError, IOError) as e: 16 | debug1("Error creating socket to notify systemd: %s\n" % e) 17 | return False 18 | 19 | if not message: 20 | return False 21 | 22 | assert isinstance(message, bytes) 23 | 24 | try: 25 | return (sock.sendto(message, addr) > 0) 26 | except (OSError, IOError) as e: 27 | debug1("Error notifying systemd: %s\n" % e) 28 | return False 29 | 30 | def send(*messages): 31 | return _notify(b'\n'.join(messages)) 32 | 33 | def ready(): 34 | return b"READY=1" 35 | 36 | def stop(): 37 | return b"STOPPING=1" 38 | 39 | def status(message): 40 | return b"STATUS=%s" % message.encode('utf8') 41 | -------------------------------------------------------------------------------- /sshuttle/server.py: -------------------------------------------------------------------------------- 1 | import re 2 | import struct 3 | import socket 4 | import traceback 5 | import time 6 | import sys 7 | import os 8 | import platform 9 | 10 | import sshuttle.ssnet as ssnet 11 | import sshuttle.helpers as helpers 12 | import sshuttle.hostwatch as hostwatch 13 | import subprocess as ssubprocess 14 | from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper 15 | from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \ 16 | resolvconf_random_nameserver 17 | 18 | try: 19 | from shutil import which 20 | except ImportError: 21 | from distutils.spawn import find_executable as which 22 | 23 | 24 | def _ipmatch(ipstr): 25 | # FIXME: IPv4 only 26 | if ipstr == 'default': 27 | ipstr = '0.0.0.0/0' 28 | m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) 29 | if m: 30 | g = m.groups() 31 | ips = g[0] 32 | width = int(g[4] or 32) 33 | if g[1] is None: 34 | ips += '.0.0.0' 35 | width = min(width, 8) 36 | elif g[2] is None: 37 | ips += '.0.0' 38 | width = min(width, 16) 39 | elif g[3] is None: 40 | ips += '.0' 41 | width = min(width, 24) 42 | ips = ips 43 | return (struct.unpack('!I', socket.inet_aton(ips))[0], width) 44 | 45 | 46 | def _ipstr(ip, width): 47 | # FIXME: IPv4 only 48 | if width >= 32: 49 | return ip 50 | else: 51 | return "%s/%d" % (ip, width) 52 | 53 | 54 | def _maskbits(netmask): 55 | # FIXME: IPv4 only 56 | if not netmask: 57 | return 32 58 | for i in range(32): 59 | if netmask[0] & _shl(1, i): 60 | return 32 - i 61 | return 0 62 | 63 | 64 | def _shl(n, bits): 65 | return n * int(2 ** bits) 66 | 67 | 68 | def _route_netstat(line): 69 | cols = line.split(None) 70 | if len(cols) < 3: 71 | return None, None 72 | ipw = _ipmatch(cols[0]) 73 | maskw = _ipmatch(cols[2]) # linux only 74 | mask = _maskbits(maskw) # returns 32 if maskw is null 75 | return ipw, mask 76 | 77 | 78 | def _route_iproute(line): 79 | ipm = line.split(None, 1)[0] 80 | if '/' not in ipm: 81 | return None, None 82 | ip, mask = ipm.split('/') 83 | ipw = _ipmatch(ip) 84 | return ipw, int(mask) 85 | 86 | 87 | def _list_routes(argv, extract_route): 88 | # FIXME: IPv4 only 89 | env = { 90 | 'PATH': os.environ['PATH'], 91 | 'LC_ALL': "C", 92 | } 93 | p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env) 94 | routes = [] 95 | for line in p.stdout: 96 | if not line.strip(): 97 | continue 98 | ipw, mask = extract_route(line.decode("ASCII")) 99 | if not ipw: 100 | continue 101 | width = min(ipw[1], mask) 102 | ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width) 103 | routes.append( 104 | (socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width)) 105 | rv = p.wait() 106 | if rv != 0: 107 | log('WARNING: %r returned %d\n' % (argv, rv)) 108 | log('WARNING: That prevents --auto-nets from working.\n') 109 | 110 | return routes 111 | 112 | 113 | def list_routes(): 114 | if which('ip'): 115 | routes = _list_routes(['ip', 'route'], _route_iproute) 116 | elif which('netstat'): 117 | routes = _list_routes(['netstat', '-rn'], _route_netstat) 118 | else: 119 | log('WARNING: Neither ip nor netstat were found on the server.\n') 120 | routes = [] 121 | 122 | for (family, ip, width) in routes: 123 | if not ip.startswith('0.') and not ip.startswith('127.'): 124 | yield (family, ip, width) 125 | 126 | 127 | def _exc_dump(): 128 | exc_info = sys.exc_info() 129 | return ''.join(traceback.format_exception(*exc_info)) 130 | 131 | 132 | def start_hostwatch(seed_hosts, auto_hosts): 133 | s1, s2 = socket.socketpair() 134 | pid = os.fork() 135 | if not pid: 136 | # child 137 | rv = 99 138 | try: 139 | try: 140 | s2.close() 141 | os.dup2(s1.fileno(), 1) 142 | os.dup2(s1.fileno(), 0) 143 | s1.close() 144 | rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0 145 | except Exception: 146 | log('%s\n' % _exc_dump()) 147 | rv = 98 148 | finally: 149 | os._exit(rv) 150 | s1.close() 151 | return pid, s2 152 | 153 | 154 | class Hostwatch: 155 | 156 | def __init__(self): 157 | self.pid = 0 158 | self.sock = None 159 | 160 | 161 | class DnsProxy(Handler): 162 | 163 | def __init__(self, mux, chan, request, to_nameserver): 164 | Handler.__init__(self, []) 165 | self.timeout = time.time() + 30 166 | self.mux = mux 167 | self.chan = chan 168 | self.tries = 0 169 | self.request = request 170 | self.peers = {} 171 | self.to_ns_peer = None 172 | self.to_ns_port = None 173 | if to_nameserver is None: 174 | self.to_nameserver = None 175 | else: 176 | self.to_ns_peer, self.to_ns_port = to_nameserver.split("@") 177 | self.to_nameserver = self._addrinfo(self.to_ns_peer, 178 | self.to_ns_port) 179 | self.try_send() 180 | 181 | @staticmethod 182 | def _addrinfo(peer, port): 183 | if int(port) == 0: 184 | port = 53 185 | family, _, _, _, sockaddr = socket.getaddrinfo(peer, port)[0] 186 | return (family, sockaddr) 187 | 188 | def try_send(self): 189 | if self.tries >= 3: 190 | return 191 | self.tries += 1 192 | 193 | if self.to_nameserver is None: 194 | _, peer = resolvconf_random_nameserver() 195 | port = 53 196 | else: 197 | peer = self.to_ns_peer 198 | port = int(self.to_ns_port) 199 | 200 | family, sockaddr = self._addrinfo(peer, port) 201 | sock = socket.socket(family, socket.SOCK_DGRAM) 202 | sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) 203 | sock.connect(sockaddr) 204 | 205 | self.peers[sock] = peer 206 | 207 | debug2('DNS: sending to %r:%d (try %d)\n' % (peer, port, self.tries)) 208 | try: 209 | sock.send(self.request) 210 | self.socks.append(sock) 211 | except socket.error: 212 | _, e = sys.exc_info()[:2] 213 | if e.args[0] in ssnet.NET_ERRS: 214 | # might have been spurious; try again. 215 | # Note: these errors sometimes are reported by recv(), 216 | # and sometimes by send(). We have to catch both. 217 | debug2('DNS send to %r: %s\n' % (peer, e)) 218 | self.try_send() 219 | return 220 | else: 221 | log('DNS send to %r: %s\n' % (peer, e)) 222 | return 223 | 224 | def callback(self, sock): 225 | peer = self.peers[sock] 226 | 227 | try: 228 | data = sock.recv(4096) 229 | except socket.error: 230 | _, e = sys.exc_info()[:2] 231 | self.socks.remove(sock) 232 | del self.peers[sock] 233 | 234 | if e.args[0] in ssnet.NET_ERRS: 235 | # might have been spurious; try again. 236 | # Note: these errors sometimes are reported by recv(), 237 | # and sometimes by send(). We have to catch both. 238 | debug2('DNS recv from %r: %s\n' % (peer, e)) 239 | self.try_send() 240 | return 241 | else: 242 | log('DNS recv from %r: %s\n' % (peer, e)) 243 | return 244 | debug2('DNS response: %d bytes\n' % len(data)) 245 | self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data) 246 | self.ok = False 247 | 248 | 249 | class UdpProxy(Handler): 250 | 251 | def __init__(self, mux, chan, family): 252 | sock = socket.socket(family, socket.SOCK_DGRAM) 253 | Handler.__init__(self, [sock]) 254 | self.timeout = time.time() + 30 255 | self.mux = mux 256 | self.chan = chan 257 | self.sock = sock 258 | if family == socket.AF_INET: 259 | self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) 260 | 261 | def send(self, dstip, data): 262 | debug2('UDP: sending to %r port %d\n' % dstip) 263 | try: 264 | self.sock.sendto(data, dstip) 265 | except socket.error: 266 | _, e = sys.exc_info()[:2] 267 | log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e)) 268 | return 269 | 270 | def callback(self, sock): 271 | try: 272 | data, peer = sock.recvfrom(4096) 273 | except socket.error: 274 | _, e = sys.exc_info()[:2] 275 | log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e)) 276 | return 277 | debug2('UDP response: %d bytes\n' % len(data)) 278 | hdr = b("%s,%r," % (peer[0], peer[1])) 279 | self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data) 280 | 281 | 282 | def main(latency_control, auto_hosts, to_nameserver): 283 | debug1('Starting server with Python version %s\n' 284 | % platform.python_version()) 285 | 286 | if helpers.verbose >= 1: 287 | helpers.logprefix = ' s: ' 288 | else: 289 | helpers.logprefix = 'server: ' 290 | debug1('latency control setting = %r\n' % latency_control) 291 | 292 | routes = list(list_routes()) 293 | debug1('available routes:\n') 294 | for r in routes: 295 | debug1(' %d/%s/%d\n' % r) 296 | 297 | # synchronization header 298 | sys.stdout.write('\0\0SSHUTTLE0001') 299 | sys.stdout.flush() 300 | 301 | handlers = [] 302 | mux = Mux(socket.fromfd(sys.stdin.fileno(), 303 | socket.AF_INET, socket.SOCK_STREAM), 304 | socket.fromfd(sys.stdout.fileno(), 305 | socket.AF_INET, socket.SOCK_STREAM)) 306 | handlers.append(mux) 307 | routepkt = '' 308 | for r in routes: 309 | routepkt += '%d,%s,%d\n' % r 310 | mux.send(0, ssnet.CMD_ROUTES, b(routepkt)) 311 | 312 | hw = Hostwatch() 313 | hw.leftover = b('') 314 | 315 | def hostwatch_ready(sock): 316 | assert(hw.pid) 317 | content = hw.sock.recv(4096) 318 | if content: 319 | lines = (hw.leftover + content).split(b('\n')) 320 | if lines[-1]: 321 | # no terminating newline: entry isn't complete yet! 322 | hw.leftover = lines.pop() 323 | lines.append(b('')) 324 | else: 325 | hw.leftover = b('') 326 | mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines)) 327 | else: 328 | raise Fatal('hostwatch process died') 329 | 330 | def got_host_req(data): 331 | if not hw.pid: 332 | (hw.pid, hw.sock) = start_hostwatch( 333 | data.decode("ASCII").strip().split(), auto_hosts) 334 | handlers.append(Handler(socks=[hw.sock], 335 | callback=hostwatch_ready)) 336 | mux.got_host_req = got_host_req 337 | 338 | def new_channel(channel, data): 339 | (family, dstip, dstport) = data.decode("ASCII").split(',', 2) 340 | family = int(family) 341 | # AF_INET is the same constant on Linux and BSD but AF_INET6 342 | # is different. As the client and server can be running on 343 | # different platforms we can not just set the socket family 344 | # to what comes in the wire. 345 | if family != socket.AF_INET: 346 | family = socket.AF_INET6 347 | dstport = int(dstport) 348 | outwrap = ssnet.connect_dst(family, dstip, dstport) 349 | handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) 350 | mux.new_channel = new_channel 351 | 352 | dnshandlers = {} 353 | 354 | def dns_req(channel, data): 355 | debug2('Incoming DNS request channel=%d.\n' % channel) 356 | h = DnsProxy(mux, channel, data, to_nameserver) 357 | handlers.append(h) 358 | dnshandlers[channel] = h 359 | mux.got_dns_req = dns_req 360 | 361 | udphandlers = {} 362 | 363 | def udp_req(channel, cmd, data): 364 | debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel, cmd)) 365 | if cmd == ssnet.CMD_UDP_DATA: 366 | (dstip, dstport, data) = data.split(b(','), 2) 367 | dstport = int(dstport) 368 | debug2('is incoming UDP data. %r %d.\n' % (dstip, dstport)) 369 | h = udphandlers[channel] 370 | h.send((dstip, dstport), data) 371 | elif cmd == ssnet.CMD_UDP_CLOSE: 372 | debug2('is incoming UDP close\n') 373 | h = udphandlers[channel] 374 | h.ok = False 375 | del mux.channels[channel] 376 | 377 | def udp_open(channel, data): 378 | debug2('Incoming UDP open.\n') 379 | family = int(data) 380 | mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data) 381 | if channel in udphandlers: 382 | raise Fatal('UDP connection channel %d already open' % channel) 383 | else: 384 | h = UdpProxy(mux, channel, family) 385 | handlers.append(h) 386 | udphandlers[channel] = h 387 | mux.got_udp_open = udp_open 388 | 389 | while mux.ok: 390 | if hw.pid: 391 | assert(hw.pid > 0) 392 | (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG) 393 | if rpid: 394 | raise Fatal( 395 | 'hostwatch exited unexpectedly: code 0x%04x\n' % rv) 396 | 397 | ssnet.runonce(handlers, mux) 398 | if latency_control: 399 | mux.check_fullness() 400 | 401 | if dnshandlers: 402 | now = time.time() 403 | remove = [] 404 | for channel, h in dnshandlers.items(): 405 | if h.timeout < now or not h.ok: 406 | debug3('expiring dnsreqs channel=%d\n' % channel) 407 | remove.append(channel) 408 | h.ok = False 409 | for channel in remove: 410 | del dnshandlers[channel] 411 | if udphandlers: 412 | remove = [] 413 | for channel, h in udphandlers.items(): 414 | if not h.ok: 415 | debug3('expiring UDP channel=%d\n' % channel) 416 | remove.append(channel) 417 | h.ok = False 418 | for channel in remove: 419 | del udphandlers[channel] 420 | -------------------------------------------------------------------------------- /sshuttle/ssh.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import re 4 | import socket 5 | import zlib 6 | import imp 7 | import subprocess as ssubprocess 8 | import shlex 9 | import sshuttle.helpers as helpers 10 | from sshuttle.helpers import debug2 11 | 12 | try: 13 | # Python >= 3.5 14 | from shlex import quote 15 | except ImportError: 16 | # Python 2.x 17 | from pipes import quote 18 | 19 | 20 | def readfile(name): 21 | tokens = name.split(".") 22 | f = None 23 | 24 | token = tokens[0] 25 | token_name = [token] 26 | token_str = ".".join(token_name) 27 | 28 | try: 29 | f, pathname, description = imp.find_module(token_str) 30 | 31 | for token in tokens[1:]: 32 | module = imp.load_module(token_str, f, pathname, description) 33 | if f is not None: 34 | f.close() 35 | 36 | token_name.append(token) 37 | token_str = ".".join(token_name) 38 | 39 | f, pathname, description = imp.find_module( 40 | token, module.__path__) 41 | 42 | if f is not None: 43 | contents = f.read() 44 | else: 45 | contents = "" 46 | 47 | finally: 48 | if f is not None: 49 | f.close() 50 | 51 | return contents.encode("UTF8") 52 | 53 | 54 | def empackage(z, name, data=None): 55 | if not data: 56 | data = readfile(name) 57 | content = z.compress(data) 58 | content += z.flush(zlib.Z_SYNC_FLUSH) 59 | 60 | return b'%s\n%d\n%s' % (name.encode("ASCII"), len(content), content) 61 | 62 | 63 | def connect(ssh_cmd, rhostport, python, stderr, options): 64 | portl = [] 65 | 66 | if re.sub(r'.*@', '', rhostport or '').count(':') > 1: 67 | if rhostport.count(']') or rhostport.count('['): 68 | result = rhostport.split(']') 69 | rhost = result[0].strip('[') 70 | if len(result) > 1: 71 | result[1] = result[1].strip(':') 72 | if result[1] != '': 73 | portl = ['-p', str(int(result[1]))] 74 | # can't disambiguate IPv6 colons and a port number. pass the hostname 75 | # through. 76 | else: 77 | rhost = rhostport 78 | else: # IPv4 79 | l = (rhostport or '').rsplit(':', 1) 80 | rhost = l[0] 81 | if len(l) > 1: 82 | portl = ['-p', str(int(l[1]))] 83 | 84 | if rhost == '-': 85 | rhost = None 86 | 87 | z = zlib.compressobj(1) 88 | content = readfile('sshuttle.assembler') 89 | optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items())) 90 | optdata = optdata.encode("UTF8") 91 | content2 = (empackage(z, 'sshuttle') + 92 | empackage(z, 'sshuttle.cmdline_options', optdata) + 93 | empackage(z, 'sshuttle.helpers') + 94 | empackage(z, 'sshuttle.ssnet') + 95 | empackage(z, 'sshuttle.hostwatch') + 96 | empackage(z, 'sshuttle.server') + 97 | b"\n") 98 | 99 | pyscript = r""" 100 | import sys, os; 101 | verbosity=%d; 102 | sys.stdin = os.fdopen(0, "rb"); 103 | exec(compile(sys.stdin.read(%d), "assembler.py", "exec")) 104 | """ % (helpers.verbose or 0, len(content)) 105 | pyscript = re.sub(r'\s+', ' ', pyscript.strip()) 106 | 107 | if not rhost: 108 | # ignore the --python argument when running locally; we already know 109 | # which python version works. 110 | argv = [sys.executable, '-c', pyscript] 111 | else: 112 | if ssh_cmd: 113 | sshl = shlex.split(ssh_cmd) 114 | else: 115 | sshl = ['ssh'] 116 | if python: 117 | pycmd = "'%s' -c '%s'" % (python, pyscript) 118 | else: 119 | pycmd = ("P=python3; $P -V 2>/dev/null || P=python; " 120 | "exec \"$P\" -c %s") % quote(pyscript) 121 | pycmd = ("exec /bin/sh -c %s" % quote(pycmd)) 122 | argv = (sshl + 123 | portl + 124 | [rhost, '--', pycmd]) 125 | (s1, s2) = socket.socketpair() 126 | 127 | def setup(): 128 | # runs in the child process 129 | s2.close() 130 | s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) 131 | s1.close() 132 | debug2('executing: %r\n' % argv) 133 | p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, 134 | close_fds=True, stderr=stderr) 135 | os.close(s1a) 136 | os.close(s1b) 137 | s2.sendall(content) 138 | s2.sendall(content2) 139 | return p, s2 140 | -------------------------------------------------------------------------------- /sshuttle/ssyslog.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import subprocess as ssubprocess 4 | 5 | 6 | _p = None 7 | 8 | 9 | def start_syslog(): 10 | global _p 11 | _p = ssubprocess.Popen(['logger', 12 | '-p', 'daemon.notice', 13 | '-t', 'sshuttle'], stdin=ssubprocess.PIPE) 14 | 15 | 16 | def stderr_to_syslog(): 17 | sys.stdout.flush() 18 | sys.stderr.flush() 19 | os.dup2(_p.stdin.fileno(), 2) 20 | -------------------------------------------------------------------------------- /sshuttle/stresstest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import socket 3 | import select 4 | import struct 5 | import time 6 | 7 | listener = socket.socket() 8 | listener.bind(('127.0.0.1', 0)) 9 | listener.listen(500) 10 | 11 | servers = [] 12 | clients = [] 13 | remain = {} 14 | 15 | NUMCLIENTS = 50 16 | count = 0 17 | 18 | 19 | while 1: 20 | if len(clients) < NUMCLIENTS: 21 | c = socket.socket() 22 | c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 23 | c.bind(('0.0.0.0', 0)) 24 | c.connect(listener.getsockname()) 25 | count += 1 26 | if count >= 16384: 27 | count = 1 28 | print('cli CREATING %d' % count) 29 | b = struct.pack('I', count) + 'x' * count 30 | remain[c] = count 31 | print('cli >> %r' % len(b)) 32 | c.send(b) 33 | c.shutdown(socket.SHUT_WR) 34 | clients.append(c) 35 | r = [listener] 36 | time.sleep(0.1) 37 | else: 38 | r = [listener] + servers + clients 39 | print('select(%d)' % len(r)) 40 | r, w, x = select.select(r, [], [], 5) 41 | assert(r) 42 | for i in r: 43 | if i == listener: 44 | s, addr = listener.accept() 45 | servers.append(s) 46 | elif i in servers: 47 | b = i.recv(4096) 48 | print('srv << %r' % len(b)) 49 | if i not in remain: 50 | assert(len(b) >= 4) 51 | want = struct.unpack('I', b[:4])[0] 52 | b = b[4:] 53 | # i.send('y'*want) 54 | else: 55 | want = remain[i] 56 | if want < len(b): 57 | print('weird wanted %d bytes, got %d: %r' % (want, len(b), b)) 58 | assert(want >= len(b)) 59 | want -= len(b) 60 | remain[i] = want 61 | if not b: # EOF 62 | if want: 63 | print('weird: eof but wanted %d more' % want) 64 | assert(want == 0) 65 | i.close() 66 | servers.remove(i) 67 | del remain[i] 68 | else: 69 | print('srv >> %r' % len(b)) 70 | i.send('y' * len(b)) 71 | if not want: 72 | i.shutdown(socket.SHUT_WR) 73 | elif i in clients: 74 | b = i.recv(4096) 75 | print('cli << %r' % len(b)) 76 | want = remain[i] 77 | if want < len(b): 78 | print('weird wanted %d bytes, got %d: %r' % (want, len(b), b)) 79 | assert(want >= len(b)) 80 | want -= len(b) 81 | remain[i] = want 82 | if not b: # EOF 83 | if want: 84 | print('weird: eof but wanted %d more' % want) 85 | assert(want == 0) 86 | i.close() 87 | clients.remove(i) 88 | del remain[i] 89 | listener.accept() 90 | -------------------------------------------------------------------------------- /sshuttle/tests/client/test_firewall.py: -------------------------------------------------------------------------------- 1 | from mock import Mock, patch, call 2 | import io 3 | from socket import AF_INET, AF_INET6 4 | 5 | import sshuttle.firewall 6 | 7 | 8 | def setup_daemon(): 9 | stdin = io.StringIO(u"""ROUTES 10 | {inet},24,0,1.2.3.0,8000,9000 11 | {inet},32,1,1.2.3.66,8080,8080 12 | {inet6},64,0,2404:6800:4004:80c::,0,0 13 | {inet6},128,1,2404:6800:4004:80c::101f,80,80 14 | NSLIST 15 | {inet},1.2.3.33 16 | {inet6},2404:6800:4004:80c::33 17 | PORTS 1024,1025,1026,1027 18 | GO 1 - 19 | HOST 1.2.3.3,existing 20 | """.format(inet=AF_INET, inet6=AF_INET6)) 21 | stdout = Mock() 22 | return stdin, stdout 23 | 24 | 25 | def test_rewrite_etc_hosts(tmpdir): 26 | orig_hosts = tmpdir.join("hosts.orig") 27 | orig_hosts.write("1.2.3.3 existing\n") 28 | 29 | new_hosts = tmpdir.join("hosts") 30 | orig_hosts.copy(new_hosts) 31 | 32 | hostmap = { 33 | 'myhost': '1.2.3.4', 34 | 'myotherhost': '1.2.3.5', 35 | } 36 | with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)): 37 | sshuttle.firewall.rewrite_etc_hosts(hostmap, 10) 38 | 39 | with new_hosts.open() as f: 40 | line = f.readline() 41 | s = line.split() 42 | assert s == ['1.2.3.3', 'existing'] 43 | 44 | line = f.readline() 45 | s = line.split() 46 | assert s == ['1.2.3.4', 'myhost', 47 | '#', 'sshuttle-firewall-10', 'AUTOCREATED'] 48 | 49 | line = f.readline() 50 | s = line.split() 51 | assert s == ['1.2.3.5', 'myotherhost', 52 | '#', 'sshuttle-firewall-10', 'AUTOCREATED'] 53 | 54 | line = f.readline() 55 | assert line == "" 56 | 57 | with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)): 58 | sshuttle.firewall.restore_etc_hosts(10) 59 | assert orig_hosts.computehash() == new_hosts.computehash() 60 | 61 | 62 | def test_subnet_weight(): 63 | subnets = [ 64 | (AF_INET, 16, 0, '192.168.0.0', 0, 0), 65 | (AF_INET, 24, 0, '192.168.69.0', 0, 0), 66 | (AF_INET, 32, 0, '192.168.69.70', 0, 0), 67 | (AF_INET, 32, 1, '192.168.69.70', 0, 0), 68 | (AF_INET, 32, 1, '192.168.69.70', 80, 80), 69 | (AF_INET, 0, 1, '0.0.0.0', 0, 0), 70 | (AF_INET, 0, 1, '0.0.0.0', 8000, 9000), 71 | (AF_INET, 0, 1, '0.0.0.0', 8000, 8500), 72 | (AF_INET, 0, 1, '0.0.0.0', 8000, 8000), 73 | (AF_INET, 0, 1, '0.0.0.0', 400, 450) 74 | ] 75 | subnets_sorted = [ 76 | (AF_INET, 32, 1, '192.168.69.70', 80, 80), 77 | (AF_INET, 0, 1, '0.0.0.0', 8000, 8000), 78 | (AF_INET, 0, 1, '0.0.0.0', 400, 450), 79 | (AF_INET, 0, 1, '0.0.0.0', 8000, 8500), 80 | (AF_INET, 0, 1, '0.0.0.0', 8000, 9000), 81 | (AF_INET, 32, 1, '192.168.69.70', 0, 0), 82 | (AF_INET, 32, 0, '192.168.69.70', 0, 0), 83 | (AF_INET, 24, 0, '192.168.69.0', 0, 0), 84 | (AF_INET, 16, 0, '192.168.0.0', 0, 0), 85 | (AF_INET, 0, 1, '0.0.0.0', 0, 0) 86 | ] 87 | 88 | assert subnets_sorted == \ 89 | sorted(subnets, key=sshuttle.firewall.subnet_weight, reverse=True) 90 | 91 | 92 | @patch('sshuttle.firewall.rewrite_etc_hosts') 93 | @patch('sshuttle.firewall.setup_daemon') 94 | @patch('sshuttle.firewall.get_method') 95 | def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): 96 | stdin, stdout = setup_daemon() 97 | mock_setup_daemon.return_value = stdin, stdout 98 | 99 | mock_get_method("not_auto").name = "test" 100 | mock_get_method.reset_mock() 101 | 102 | sshuttle.firewall.main("not_auto", False) 103 | 104 | assert mock_rewrite_etc_hosts.mock_calls == [ 105 | call({'1.2.3.3': 'existing'}, 1024), 106 | call({}, 1024), 107 | ] 108 | 109 | assert stdout.mock_calls == [ 110 | call.write('READY test\n'), 111 | call.flush(), 112 | call.write('STARTED\n'), 113 | call.flush() 114 | ] 115 | assert mock_setup_daemon.mock_calls == [call()] 116 | assert mock_get_method.mock_calls == [ 117 | call('not_auto'), 118 | call().setup_firewall( 119 | 1024, 1026, 120 | [(AF_INET6, u'2404:6800:4004:80c::33')], 121 | AF_INET6, 122 | [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), 123 | (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], 124 | True, 125 | None), 126 | call().setup_firewall( 127 | 1025, 1027, 128 | [(AF_INET, u'1.2.3.33')], 129 | AF_INET, 130 | [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), 131 | (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], 132 | True, 133 | None), 134 | call().restore_firewall(1024, AF_INET6, True, None), 135 | call().restore_firewall(1025, AF_INET, True, None), 136 | ] 137 | -------------------------------------------------------------------------------- /sshuttle/tests/client/test_helpers.py: -------------------------------------------------------------------------------- 1 | from mock import patch, call 2 | import sys 3 | import io 4 | import socket 5 | from socket import AF_INET, AF_INET6 6 | import errno 7 | 8 | import sshuttle.helpers 9 | 10 | 11 | @patch('sshuttle.helpers.logprefix', new='prefix: ') 12 | @patch('sshuttle.helpers.sys.stdout') 13 | @patch('sshuttle.helpers.sys.stderr') 14 | def test_log(mock_stderr, mock_stdout): 15 | sshuttle.helpers.log("message") 16 | sshuttle.helpers.log("abc") 17 | sshuttle.helpers.log("message 1\n") 18 | sshuttle.helpers.log("message 2\nline2\nline3\n") 19 | sshuttle.helpers.log("message 3\nline2\nline3") 20 | assert mock_stdout.mock_calls == [ 21 | call.flush(), 22 | call.flush(), 23 | call.flush(), 24 | call.flush(), 25 | call.flush(), 26 | ] 27 | assert mock_stderr.mock_calls == [ 28 | call.write('prefix: message'), 29 | call.flush(), 30 | call.write('prefix: abc'), 31 | call.flush(), 32 | call.write('prefix: message 1\n'), 33 | call.flush(), 34 | call.write('prefix: message 2\n'), 35 | call.write('---> line2\n'), 36 | call.write('---> line3\n'), 37 | call.flush(), 38 | call.write('prefix: message 3\n'), 39 | call.write('---> line2\n'), 40 | call.write('---> line3\n'), 41 | call.flush(), 42 | ] 43 | 44 | 45 | @patch('sshuttle.helpers.logprefix', new='prefix: ') 46 | @patch('sshuttle.helpers.verbose', new=1) 47 | @patch('sshuttle.helpers.sys.stdout') 48 | @patch('sshuttle.helpers.sys.stderr') 49 | def test_debug1(mock_stderr, mock_stdout): 50 | sshuttle.helpers.debug1("message") 51 | assert mock_stdout.mock_calls == [ 52 | call.flush(), 53 | ] 54 | assert mock_stderr.mock_calls == [ 55 | call.write('prefix: message'), 56 | call.flush(), 57 | ] 58 | 59 | 60 | @patch('sshuttle.helpers.logprefix', new='prefix: ') 61 | @patch('sshuttle.helpers.verbose', new=0) 62 | @patch('sshuttle.helpers.sys.stdout') 63 | @patch('sshuttle.helpers.sys.stderr') 64 | def test_debug1_nop(mock_stderr, mock_stdout): 65 | sshuttle.helpers.debug1("message") 66 | assert mock_stdout.mock_calls == [] 67 | assert mock_stderr.mock_calls == [] 68 | 69 | 70 | @patch('sshuttle.helpers.logprefix', new='prefix: ') 71 | @patch('sshuttle.helpers.verbose', new=2) 72 | @patch('sshuttle.helpers.sys.stdout') 73 | @patch('sshuttle.helpers.sys.stderr') 74 | def test_debug2(mock_stderr, mock_stdout): 75 | sshuttle.helpers.debug2("message") 76 | assert mock_stdout.mock_calls == [ 77 | call.flush(), 78 | ] 79 | assert mock_stderr.mock_calls == [ 80 | call.write('prefix: message'), 81 | call.flush(), 82 | ] 83 | 84 | 85 | @patch('sshuttle.helpers.logprefix', new='prefix: ') 86 | @patch('sshuttle.helpers.verbose', new=1) 87 | @patch('sshuttle.helpers.sys.stdout') 88 | @patch('sshuttle.helpers.sys.stderr') 89 | def test_debug2_nop(mock_stderr, mock_stdout): 90 | sshuttle.helpers.debug2("message") 91 | assert mock_stdout.mock_calls == [] 92 | assert mock_stderr.mock_calls == [] 93 | 94 | 95 | @patch('sshuttle.helpers.logprefix', new='prefix: ') 96 | @patch('sshuttle.helpers.verbose', new=3) 97 | @patch('sshuttle.helpers.sys.stdout') 98 | @patch('sshuttle.helpers.sys.stderr') 99 | def test_debug3(mock_stderr, mock_stdout): 100 | sshuttle.helpers.debug3("message") 101 | assert mock_stdout.mock_calls == [ 102 | call.flush(), 103 | ] 104 | assert mock_stderr.mock_calls == [ 105 | call.write('prefix: message'), 106 | call.flush(), 107 | ] 108 | 109 | 110 | @patch('sshuttle.helpers.logprefix', new='prefix: ') 111 | @patch('sshuttle.helpers.verbose', new=2) 112 | @patch('sshuttle.helpers.sys.stdout') 113 | @patch('sshuttle.helpers.sys.stderr') 114 | def test_debug3_nop(mock_stderr, mock_stdout): 115 | sshuttle.helpers.debug3("message") 116 | assert mock_stdout.mock_calls == [] 117 | assert mock_stderr.mock_calls == [] 118 | 119 | 120 | @patch('sshuttle.helpers.open', create=True) 121 | def test_resolvconf_nameservers(mock_open): 122 | mock_open.return_value = io.StringIO(u""" 123 | # Generated by NetworkManager 124 | search pri 125 | nameserver 192.168.1.1 126 | nameserver 192.168.2.1 127 | nameserver 192.168.3.1 128 | nameserver 192.168.4.1 129 | nameserver 2404:6800:4004:80c::1 130 | nameserver 2404:6800:4004:80c::2 131 | nameserver 2404:6800:4004:80c::3 132 | nameserver 2404:6800:4004:80c::4 133 | """) 134 | 135 | ns = sshuttle.helpers.resolvconf_nameservers() 136 | assert ns == [ 137 | (AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'), 138 | (AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'), 139 | (AF_INET6, u'2404:6800:4004:80c::1'), 140 | (AF_INET6, u'2404:6800:4004:80c::2'), 141 | (AF_INET6, u'2404:6800:4004:80c::3'), 142 | (AF_INET6, u'2404:6800:4004:80c::4') 143 | ] 144 | 145 | 146 | @patch('sshuttle.helpers.open', create=True) 147 | def test_resolvconf_random_nameserver(mock_open): 148 | mock_open.return_value = io.StringIO(u""" 149 | # Generated by NetworkManager 150 | search pri 151 | nameserver 192.168.1.1 152 | nameserver 192.168.2.1 153 | nameserver 192.168.3.1 154 | nameserver 192.168.4.1 155 | nameserver 2404:6800:4004:80c::1 156 | nameserver 2404:6800:4004:80c::2 157 | nameserver 2404:6800:4004:80c::3 158 | nameserver 2404:6800:4004:80c::4 159 | """) 160 | ns = sshuttle.helpers.resolvconf_random_nameserver() 161 | assert ns in [ 162 | (AF_INET, u'192.168.1.1'), (AF_INET, u'192.168.2.1'), 163 | (AF_INET, u'192.168.3.1'), (AF_INET, u'192.168.4.1'), 164 | (AF_INET6, u'2404:6800:4004:80c::1'), 165 | (AF_INET6, u'2404:6800:4004:80c::2'), 166 | (AF_INET6, u'2404:6800:4004:80c::3'), 167 | (AF_INET6, u'2404:6800:4004:80c::4') 168 | ] 169 | 170 | 171 | @patch('sshuttle.helpers.socket.socket.bind') 172 | def test_islocal(mock_bind): 173 | bind_error = socket.error(errno.EADDRNOTAVAIL) 174 | mock_bind.side_effect = [None, bind_error, None, bind_error] 175 | 176 | assert sshuttle.helpers.islocal("127.0.0.1", AF_INET) 177 | assert not sshuttle.helpers.islocal("192.0.2.1", AF_INET) 178 | assert sshuttle.helpers.islocal("::1", AF_INET6) 179 | assert not sshuttle.helpers.islocal("2001:db8::1", AF_INET6) 180 | 181 | 182 | def test_family_ip_tuple(): 183 | assert sshuttle.helpers.family_ip_tuple("127.0.0.1") \ 184 | == (AF_INET, "127.0.0.1") 185 | assert sshuttle.helpers.family_ip_tuple("192.168.2.6") \ 186 | == (AF_INET, "192.168.2.6") 187 | assert sshuttle.helpers.family_ip_tuple("::1") \ 188 | == (AF_INET6, "::1") 189 | assert sshuttle.helpers.family_ip_tuple("2404:6800:4004:80c::1") \ 190 | == (AF_INET6, "2404:6800:4004:80c::1") 191 | 192 | 193 | def test_family_to_string(): 194 | assert sshuttle.helpers.family_to_string(AF_INET) == "AF_INET" 195 | assert sshuttle.helpers.family_to_string(AF_INET6) == "AF_INET6" 196 | if sys.version_info < (3, 0): 197 | expected = "1" 198 | assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == "1" 199 | else: 200 | expected = 'AddressFamily.AF_UNIX' 201 | assert sshuttle.helpers.family_to_string(socket.AF_UNIX) == expected 202 | -------------------------------------------------------------------------------- /sshuttle/tests/client/test_methods_nat.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mock import Mock, patch, call 3 | import socket 4 | from socket import AF_INET, AF_INET6 5 | import struct 6 | 7 | from sshuttle.helpers import Fatal 8 | from sshuttle.methods import get_method 9 | 10 | 11 | def test_get_supported_features(): 12 | method = get_method('nat') 13 | features = method.get_supported_features() 14 | assert not features.ipv6 15 | assert not features.udp 16 | assert features.dns 17 | 18 | 19 | def test_get_tcp_dstip(): 20 | sock = Mock() 21 | sock.getsockopt.return_value = struct.pack( 22 | '!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1) 23 | method = get_method('nat') 24 | assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024) 25 | assert sock.mock_calls == [call.getsockopt(0, 80, 16)] 26 | 27 | 28 | def test_recv_udp(): 29 | sock = Mock() 30 | sock.recvfrom.return_value = "11111", "127.0.0.1" 31 | method = get_method('nat') 32 | result = method.recv_udp(sock, 1024) 33 | assert sock.mock_calls == [call.recvfrom(1024)] 34 | assert result == ("127.0.0.1", None, "11111") 35 | 36 | 37 | def test_send_udp(): 38 | sock = Mock() 39 | method = get_method('nat') 40 | method.send_udp(sock, None, "127.0.0.1", "22222") 41 | assert sock.mock_calls == [call.sendto("22222", "127.0.0.1")] 42 | 43 | 44 | def test_setup_tcp_listener(): 45 | listener = Mock() 46 | method = get_method('nat') 47 | method.setup_tcp_listener(listener) 48 | assert listener.mock_calls == [] 49 | 50 | 51 | def test_setup_udp_listener(): 52 | listener = Mock() 53 | method = get_method('nat') 54 | method.setup_udp_listener(listener) 55 | assert listener.mock_calls == [] 56 | 57 | 58 | def test_assert_features(): 59 | method = get_method('nat') 60 | features = method.get_supported_features() 61 | method.assert_features(features) 62 | 63 | features.udp = True 64 | with pytest.raises(Fatal): 65 | method.assert_features(features) 66 | 67 | features.ipv6 = True 68 | with pytest.raises(Fatal): 69 | method.assert_features(features) 70 | 71 | 72 | def test_firewall_command(): 73 | method = get_method('nat') 74 | assert not method.firewall_command("somthing") 75 | 76 | 77 | @patch('sshuttle.methods.nat.ipt') 78 | @patch('sshuttle.methods.nat.ipt_ttl') 79 | @patch('sshuttle.methods.nat.ipt_chain_exists') 80 | def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): 81 | mock_ipt_chain_exists.return_value = True 82 | method = get_method('nat') 83 | assert method.name == 'nat' 84 | 85 | with pytest.raises(Exception) as excinfo: 86 | method.setup_firewall( 87 | 1024, 1026, 88 | [(AF_INET6, u'2404:6800:4004:80c::33')], 89 | AF_INET6, 90 | [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), 91 | (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], 92 | True, 93 | None) 94 | assert str(excinfo.value) \ 95 | == 'Address family "AF_INET6" unsupported by nat method_name' 96 | assert mock_ipt_chain_exists.mock_calls == [] 97 | assert mock_ipt_ttl.mock_calls == [] 98 | assert mock_ipt.mock_calls == [] 99 | 100 | with pytest.raises(Exception) as excinfo: 101 | method.setup_firewall( 102 | 1025, 1027, 103 | [(AF_INET, u'1.2.3.33')], 104 | AF_INET, 105 | [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), 106 | (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], 107 | True, 108 | None) 109 | assert str(excinfo.value) == 'UDP not supported by nat method_name' 110 | assert mock_ipt_chain_exists.mock_calls == [] 111 | assert mock_ipt_ttl.mock_calls == [] 112 | assert mock_ipt.mock_calls == [] 113 | 114 | method.setup_firewall( 115 | 1025, 1027, 116 | [(AF_INET, u'1.2.3.33')], 117 | AF_INET, 118 | [(AF_INET, 24, False, u'1.2.3.0', 8000, 9000), 119 | (AF_INET, 32, True, u'1.2.3.66', 8080, 8080)], 120 | False, 121 | None) 122 | assert mock_ipt_chain_exists.mock_calls == [ 123 | call(AF_INET, 'nat', 'sshuttle-1025') 124 | ] 125 | assert mock_ipt_ttl.mock_calls == [ 126 | call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', 127 | '--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000', 128 | '--to-ports', '1025'), 129 | call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', 130 | '--dest', u'1.2.3.33/32', '-p', 'udp', 131 | '--dport', '53', '--to-ports', '1027') 132 | ] 133 | assert mock_ipt.mock_calls == [ 134 | call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'), 135 | call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'), 136 | call(AF_INET, 'nat', '-F', 'sshuttle-1025'), 137 | call(AF_INET, 'nat', '-X', 'sshuttle-1025'), 138 | call(AF_INET, 'nat', '-N', 'sshuttle-1025'), 139 | call(AF_INET, 'nat', '-F', 'sshuttle-1025'), 140 | call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'), 141 | call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'), 142 | call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', 143 | '--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080') 144 | ] 145 | mock_ipt_chain_exists.reset_mock() 146 | mock_ipt_ttl.reset_mock() 147 | mock_ipt.reset_mock() 148 | 149 | method.restore_firewall(1025, AF_INET, False, None) 150 | assert mock_ipt_chain_exists.mock_calls == [ 151 | call(AF_INET, 'nat', 'sshuttle-1025') 152 | ] 153 | assert mock_ipt_ttl.mock_calls == [] 154 | assert mock_ipt.mock_calls == [ 155 | call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'), 156 | call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'), 157 | call(AF_INET, 'nat', '-F', 'sshuttle-1025'), 158 | call(AF_INET, 'nat', '-X', 'sshuttle-1025') 159 | ] 160 | mock_ipt_chain_exists.reset_mock() 161 | mock_ipt_ttl.reset_mock() 162 | mock_ipt.reset_mock() 163 | -------------------------------------------------------------------------------- /sshuttle/tests/client/test_methods_tproxy.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from socket import AF_INET, AF_INET6 3 | 4 | from mock import Mock, patch, call 5 | 6 | from sshuttle.methods import get_method 7 | 8 | 9 | @patch("sshuttle.methods.tproxy.recvmsg") 10 | def test_get_supported_features_recvmsg(mock_recvmsg): 11 | method = get_method('tproxy') 12 | features = method.get_supported_features() 13 | assert features.ipv6 14 | assert features.udp 15 | assert features.dns 16 | 17 | 18 | @patch("sshuttle.methods.tproxy.recvmsg", None) 19 | def test_get_supported_features_norecvmsg(): 20 | method = get_method('tproxy') 21 | features = method.get_supported_features() 22 | assert features.ipv6 23 | assert not features.udp 24 | assert not features.dns 25 | 26 | 27 | def test_get_tcp_dstip(): 28 | sock = Mock() 29 | sock.getsockname.return_value = ('127.0.0.1', 1024) 30 | method = get_method('tproxy') 31 | assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024) 32 | assert sock.mock_calls == [call.getsockname()] 33 | 34 | 35 | @patch("sshuttle.methods.tproxy.recv_udp") 36 | def test_recv_udp(mock_recv_udp): 37 | mock_recv_udp.return_value = ("127.0.0.1", "127.0.0.2", "11111") 38 | 39 | sock = Mock() 40 | method = get_method('tproxy') 41 | result = method.recv_udp(sock, 1024) 42 | assert sock.mock_calls == [] 43 | assert mock_recv_udp.mock_calls == [call(sock, 1024)] 44 | assert result == ("127.0.0.1", "127.0.0.2", "11111") 45 | 46 | 47 | @patch("sshuttle.methods.socket.socket") 48 | def test_send_udp(mock_socket): 49 | sock = Mock() 50 | method = get_method('tproxy') 51 | method.send_udp(sock, "127.0.0.2", "127.0.0.1", "2222222") 52 | assert sock.mock_calls == [] 53 | assert mock_socket.mock_calls == [ 54 | call(sock.family, 2), 55 | call().setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), 56 | call().setsockopt(0, 19, 1), 57 | call().bind('127.0.0.2'), 58 | call().sendto("2222222", '127.0.0.1'), 59 | call().close() 60 | ] 61 | 62 | 63 | def test_setup_tcp_listener(): 64 | listener = Mock() 65 | method = get_method('tproxy') 66 | method.setup_tcp_listener(listener) 67 | assert listener.mock_calls == [ 68 | call.setsockopt(0, 19, 1) 69 | ] 70 | 71 | 72 | def test_setup_udp_listener(): 73 | listener = Mock() 74 | method = get_method('tproxy') 75 | method.setup_udp_listener(listener) 76 | assert listener.mock_calls == [ 77 | call.setsockopt(0, 19, 1), 78 | call.v4.setsockopt(0, 20, 1), 79 | call.v6.setsockopt(41, 74, 1) 80 | ] 81 | 82 | 83 | def test_assert_features(): 84 | method = get_method('tproxy') 85 | features = method.get_supported_features() 86 | method.assert_features(features) 87 | 88 | 89 | def test_firewall_command(): 90 | method = get_method('tproxy') 91 | assert not method.firewall_command("somthing") 92 | 93 | 94 | @patch('sshuttle.methods.tproxy.ipt') 95 | @patch('sshuttle.methods.tproxy.ipt_ttl') 96 | @patch('sshuttle.methods.tproxy.ipt_chain_exists') 97 | def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): 98 | mock_ipt_chain_exists.return_value = True 99 | method = get_method('tproxy') 100 | assert method.name == 'tproxy' 101 | 102 | # IPV6 103 | 104 | method.setup_firewall( 105 | 1024, 1026, 106 | [(AF_INET6, u'2404:6800:4004:80c::33')], 107 | AF_INET6, 108 | [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 8000, 9000), 109 | (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)], 110 | True, 111 | None) 112 | assert mock_ipt_chain_exists.mock_calls == [ 113 | call(AF_INET6, 'mangle', 'sshuttle-m-1024'), 114 | call(AF_INET6, 'mangle', 'sshuttle-t-1024'), 115 | call(AF_INET6, 'mangle', 'sshuttle-d-1024') 116 | ] 117 | assert mock_ipt_ttl.mock_calls == [] 118 | assert mock_ipt.mock_calls == [ 119 | call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1024'), 120 | call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'), 121 | call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1024'), 122 | call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1024'), 123 | call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'), 124 | call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1024'), 125 | call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'), 126 | call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1024'), 127 | call(AF_INET6, 'mangle', '-N', 'sshuttle-m-1024'), 128 | call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1024'), 129 | call(AF_INET6, 'mangle', '-N', 'sshuttle-d-1024'), 130 | call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1024'), 131 | call(AF_INET6, 'mangle', '-N', 'sshuttle-t-1024'), 132 | call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1024'), 133 | call(AF_INET6, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1024'), 134 | call(AF_INET6, 'mangle', '-I', 'PREROUTING', '1', '-j', 135 | 'sshuttle-t-1024'), 136 | call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'MARK', 137 | '--set-mark', '1'), 138 | call(AF_INET6, 'mangle', '-A', 'sshuttle-d-1024', '-j', 'ACCEPT'), 139 | call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', 140 | '-j', 'sshuttle-d-1024', '-m', 'tcp', '-p', 'tcp'), 141 | call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-m', 'socket', 142 | '-j', 'sshuttle-d-1024', '-m', 'udp', '-p', 'udp'), 143 | call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', 144 | '--set-mark', '1', '--dest', u'2404:6800:4004:80c::33/32', 145 | '-m', 'udp', '-p', 'udp', '--dport', '53'), 146 | call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', 147 | '--tproxy-mark', '0x1/0x1', 148 | '--dest', u'2404:6800:4004:80c::33/32', 149 | '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), 150 | call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', 151 | '--dest', u'2404:6800:4004:80c::101f/128', 152 | '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'), 153 | call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', 154 | '--dest', u'2404:6800:4004:80c::101f/128', 155 | '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'), 156 | call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', 157 | '--dest', u'2404:6800:4004:80c::101f/128', 158 | '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'), 159 | call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', 160 | '--dest', u'2404:6800:4004:80c::101f/128', 161 | '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'), 162 | call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', 163 | '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', 164 | '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'), 165 | call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', 166 | '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', 167 | '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000', 168 | '--on-port', '1024'), 169 | call(AF_INET6, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', 170 | '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', 171 | '-m', 'udp', '-p', 'udp'), 172 | call(AF_INET6, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', 173 | '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', 174 | '-m', 'udp', '-p', 'udp', '--dport', '8000:9000', 175 | '--on-port', '1024') 176 | ] 177 | mock_ipt_chain_exists.reset_mock() 178 | mock_ipt_ttl.reset_mock() 179 | mock_ipt.reset_mock() 180 | 181 | method.restore_firewall(1025, AF_INET6, True, None) 182 | assert mock_ipt_chain_exists.mock_calls == [ 183 | call(AF_INET6, 'mangle', 'sshuttle-m-1025'), 184 | call(AF_INET6, 'mangle', 'sshuttle-t-1025'), 185 | call(AF_INET6, 'mangle', 'sshuttle-d-1025') 186 | ] 187 | assert mock_ipt_ttl.mock_calls == [] 188 | assert mock_ipt.mock_calls == [ 189 | call(AF_INET6, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), 190 | call(AF_INET6, 'mangle', '-F', 'sshuttle-m-1025'), 191 | call(AF_INET6, 'mangle', '-X', 'sshuttle-m-1025'), 192 | call(AF_INET6, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), 193 | call(AF_INET6, 'mangle', '-F', 'sshuttle-t-1025'), 194 | call(AF_INET6, 'mangle', '-X', 'sshuttle-t-1025'), 195 | call(AF_INET6, 'mangle', '-F', 'sshuttle-d-1025'), 196 | call(AF_INET6, 'mangle', '-X', 'sshuttle-d-1025') 197 | ] 198 | mock_ipt_chain_exists.reset_mock() 199 | mock_ipt_ttl.reset_mock() 200 | mock_ipt.reset_mock() 201 | 202 | # IPV4 203 | 204 | method.setup_firewall( 205 | 1025, 1027, 206 | [(AF_INET, u'1.2.3.33')], 207 | AF_INET, 208 | [(AF_INET, 24, False, u'1.2.3.0', 0, 0), 209 | (AF_INET, 32, True, u'1.2.3.66', 80, 80)], 210 | True, 211 | None) 212 | assert mock_ipt_chain_exists.mock_calls == [ 213 | call(AF_INET, 'mangle', 'sshuttle-m-1025'), 214 | call(AF_INET, 'mangle', 'sshuttle-t-1025'), 215 | call(AF_INET, 'mangle', 'sshuttle-d-1025') 216 | ] 217 | assert mock_ipt_ttl.mock_calls == [] 218 | assert mock_ipt.mock_calls == [ 219 | call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), 220 | call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'), 221 | call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'), 222 | call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), 223 | call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'), 224 | call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'), 225 | call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'), 226 | call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025'), 227 | call(AF_INET, 'mangle', '-N', 'sshuttle-m-1025'), 228 | call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'), 229 | call(AF_INET, 'mangle', '-N', 'sshuttle-d-1025'), 230 | call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'), 231 | call(AF_INET, 'mangle', '-N', 'sshuttle-t-1025'), 232 | call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'), 233 | call(AF_INET, 'mangle', '-I', 'OUTPUT', '1', '-j', 'sshuttle-m-1025'), 234 | call(AF_INET, 'mangle', '-I', 'PREROUTING', '1', '-j', 235 | 'sshuttle-t-1025'), 236 | call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', 237 | '-j', 'MARK', '--set-mark', '1'), 238 | call(AF_INET, 'mangle', '-A', 'sshuttle-d-1025', '-j', 'ACCEPT'), 239 | call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', 240 | '-j', 'sshuttle-d-1025', '-m', 'tcp', '-p', 'tcp'), 241 | call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-m', 'socket', 242 | '-j', 'sshuttle-d-1025', '-m', 'udp', '-p', 'udp'), 243 | call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', 244 | '--set-mark', '1', '--dest', u'1.2.3.33/32', 245 | '-m', 'udp', '-p', 'udp', '--dport', '53'), 246 | call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', 247 | '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32', 248 | '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'), 249 | call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', 250 | '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp', 251 | '--dport', '80:80'), 252 | call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', 253 | '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp', 254 | '--dport', '80:80'), 255 | call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', 256 | '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp', 257 | '--dport', '80:80'), 258 | call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', 259 | '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp', 260 | '--dport', '80:80'), 261 | call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', 262 | '--set-mark', '1', '--dest', u'1.2.3.0/24', 263 | '-m', 'tcp', '-p', 'tcp'), 264 | call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', 265 | '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', 266 | '-m', 'tcp', '-p', 'tcp', '--on-port', '1025'), 267 | call(AF_INET, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', 268 | '--set-mark', '1', '--dest', u'1.2.3.0/24', 269 | '-m', 'udp', '-p', 'udp'), 270 | call(AF_INET, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'TPROXY', 271 | '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.0/24', 272 | '-m', 'udp', '-p', 'udp', '--on-port', '1025') 273 | ] 274 | mock_ipt_chain_exists.reset_mock() 275 | mock_ipt_ttl.reset_mock() 276 | mock_ipt.reset_mock() 277 | 278 | method.restore_firewall(1025, AF_INET, True, None) 279 | assert mock_ipt_chain_exists.mock_calls == [ 280 | call(AF_INET, 'mangle', 'sshuttle-m-1025'), 281 | call(AF_INET, 'mangle', 'sshuttle-t-1025'), 282 | call(AF_INET, 'mangle', 'sshuttle-d-1025') 283 | ] 284 | assert mock_ipt_ttl.mock_calls == [] 285 | assert mock_ipt.mock_calls == [ 286 | call(AF_INET, 'mangle', '-D', 'OUTPUT', '-j', 'sshuttle-m-1025'), 287 | call(AF_INET, 'mangle', '-F', 'sshuttle-m-1025'), 288 | call(AF_INET, 'mangle', '-X', 'sshuttle-m-1025'), 289 | call(AF_INET, 'mangle', '-D', 'PREROUTING', '-j', 'sshuttle-t-1025'), 290 | call(AF_INET, 'mangle', '-F', 'sshuttle-t-1025'), 291 | call(AF_INET, 'mangle', '-X', 'sshuttle-t-1025'), 292 | call(AF_INET, 'mangle', '-F', 'sshuttle-d-1025'), 293 | call(AF_INET, 'mangle', '-X', 'sshuttle-d-1025') 294 | ] 295 | mock_ipt_chain_exists.reset_mock() 296 | mock_ipt_ttl.reset_mock() 297 | mock_ipt.reset_mock() 298 | -------------------------------------------------------------------------------- /sshuttle/tests/client/test_options.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import pytest 3 | import sshuttle.options 4 | from argparse import ArgumentTypeError as Fatal 5 | 6 | _ip4_reprs = { 7 | '0.0.0.0': '0.0.0.0', 8 | '255.255.255.255': '255.255.255.255', 9 | '10.0': '10.0.0.0', 10 | '184.172.10.74': '184.172.10.74', 11 | '3098282570': '184.172.10.74', 12 | '0xb8.0xac.0x0a.0x4a': '184.172.10.74', 13 | '0270.0254.0012.0112': '184.172.10.74', 14 | 'localhost': '127.0.0.1' 15 | } 16 | 17 | _ip4_swidths = (1, 8, 22, 27, 32) 18 | 19 | _ip6_reprs = { 20 | '::': '::', 21 | '::1': '::1', 22 | 'fc00::': 'fc00::', 23 | '2a01:7e00:e000:188::1': '2a01:7e00:e000:188::1' 24 | } 25 | 26 | _ip6_swidths = (48, 64, 96, 115, 128) 27 | 28 | def test_parse_subnetport_ip4(): 29 | for ip_repr, ip in _ip4_reprs.items(): 30 | assert sshuttle.options.parse_subnetport(ip_repr) \ 31 | == (socket.AF_INET, ip, 32, 0, 0) 32 | with pytest.raises(Fatal) as excinfo: 33 | sshuttle.options.parse_subnetport('10.256.0.0') 34 | assert str(excinfo.value) == 'Unable to resolve address: 10.256.0.0' 35 | 36 | 37 | def test_parse_subnetport_ip4_with_mask(): 38 | for ip_repr, ip in _ip4_reprs.items(): 39 | for swidth in _ip4_swidths: 40 | assert sshuttle.options.parse_subnetport( 41 | '/'.join((ip_repr, str(swidth))) 42 | ) == (socket.AF_INET, ip, swidth, 0, 0) 43 | assert sshuttle.options.parse_subnetport('0/0') \ 44 | == (socket.AF_INET, '0.0.0.0', 0, 0, 0) 45 | with pytest.raises(Fatal) as excinfo: 46 | sshuttle.options.parse_subnetport('10.0.0.0/33') 47 | assert str(excinfo.value) == 'width 33 is not between 0 and 32' 48 | 49 | 50 | def test_parse_subnetport_ip4_with_port(): 51 | for ip_repr, ip in _ip4_reprs.items(): 52 | assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \ 53 | == (socket.AF_INET, ip, 32, 80, 80) 54 | assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90')))\ 55 | == (socket.AF_INET, ip, 32, 80, 90) 56 | 57 | 58 | def test_parse_subnetport_ip4_with_mask_and_port(): 59 | for ip_repr, ip in _ip4_reprs.items(): 60 | assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \ 61 | == (socket.AF_INET, ip, 32, 80, 80) 62 | assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \ 63 | == (socket.AF_INET, ip, 16, 80, 90) 64 | 65 | 66 | def test_parse_subnetport_ip6(): 67 | for ip_repr, ip in _ip6_reprs.items(): 68 | assert sshuttle.options.parse_subnetport(ip_repr) \ 69 | == (socket.AF_INET6, ip, 128, 0, 0) 70 | 71 | 72 | def test_parse_subnetport_ip6_with_mask(): 73 | for ip_repr, ip in _ip6_reprs.items(): 74 | for swidth in _ip4_swidths + _ip6_swidths: 75 | assert sshuttle.options.parse_subnetport( 76 | '/'.join((ip_repr, str(swidth))) 77 | ) == (socket.AF_INET6, ip, swidth, 0, 0) 78 | assert sshuttle.options.parse_subnetport('::/0') \ 79 | == (socket.AF_INET6, '::', 0, 0, 0) 80 | with pytest.raises(Fatal) as excinfo: 81 | sshuttle.options.parse_subnetport('fc00::/129') 82 | assert str(excinfo.value) == 'width 129 is not between 0 and 128' 83 | 84 | 85 | def test_parse_subnetport_ip6_with_port(): 86 | for ip_repr, ip in _ip6_reprs.items(): 87 | assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \ 88 | == (socket.AF_INET6, ip, 128, 80, 80) 89 | assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \ 90 | == (socket.AF_INET6, ip, 128, 80, 90) 91 | 92 | 93 | def test_parse_subnetport_ip6_with_mask_and_port(): 94 | for ip_repr, ip in _ip6_reprs.items(): 95 | assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \ 96 | == (socket.AF_INET6, ip, 128, 80, 80) 97 | assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90')\ 98 | == (socket.AF_INET6, ip, 16, 80, 90) 99 | -------------------------------------------------------------------------------- /sshuttle/tests/client/test_sdnotify.py: -------------------------------------------------------------------------------- 1 | from mock import Mock, patch, call 2 | import socket 3 | 4 | import sshuttle.sdnotify 5 | 6 | 7 | @patch('sshuttle.sdnotify.os.environ.get') 8 | def test_notify_invalid_socket_path(mock_get): 9 | mock_get.return_value = 'invalid_path' 10 | assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready()) 11 | 12 | 13 | @patch('sshuttle.sdnotify.os.environ.get') 14 | def test_notify_socket_not_there(mock_get): 15 | mock_get.return_value = '/run/valid_nonexistent_path' 16 | assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready()) 17 | 18 | 19 | @patch('sshuttle.sdnotify.os.environ.get') 20 | def test_notify_no_message(mock_get): 21 | mock_get.return_value = '/run/valid_path' 22 | assert not sshuttle.sdnotify.send() 23 | 24 | 25 | @patch('sshuttle.sdnotify.socket.socket') 26 | @patch('sshuttle.sdnotify.os.environ.get') 27 | def test_notify_socket_error(mock_get, mock_socket): 28 | mock_get.return_value = '/run/valid_path' 29 | mock_socket.side_effect = socket.error('test error') 30 | assert not sshuttle.sdnotify.send(sshuttle.sdnotify.ready()) 31 | 32 | 33 | @patch('sshuttle.sdnotify.socket.socket') 34 | @patch('sshuttle.sdnotify.os.environ.get') 35 | def test_notify_sendto_error(mock_get, mock_socket): 36 | message = sshuttle.sdnotify.ready() 37 | socket_path = '/run/valid_path' 38 | 39 | sock = Mock() 40 | sock.sendto.side_effect = socket.error('test error') 41 | mock_get.return_value = '/run/valid_path' 42 | mock_socket.return_value = sock 43 | 44 | assert not sshuttle.sdnotify.send(message) 45 | assert sock.sendto.mock_calls == [ 46 | call(message, socket_path), 47 | ] 48 | 49 | 50 | @patch('sshuttle.sdnotify.socket.socket') 51 | @patch('sshuttle.sdnotify.os.environ.get') 52 | def test_notify(mock_get, mock_socket): 53 | messages = [sshuttle.sdnotify.ready(), sshuttle.sdnotify.status('Running')] 54 | socket_path = '/run/valid_path' 55 | 56 | sock = Mock() 57 | sock.sendto.return_value = 1 58 | mock_get.return_value = '/run/valid_path' 59 | mock_socket.return_value = sock 60 | 61 | assert sshuttle.sdnotify.send(*messages) 62 | assert sock.sendto.mock_calls == [ 63 | call(b'\n'.join(messages), socket_path), 64 | ] 65 | -------------------------------------------------------------------------------- /sshuttle/tests/server/test_server.py: -------------------------------------------------------------------------------- 1 | import io 2 | import socket 3 | import sshuttle.server 4 | from mock import patch, Mock 5 | 6 | 7 | def test__ipmatch(): 8 | assert sshuttle.server._ipmatch("1.2.3.4") is not None 9 | assert sshuttle.server._ipmatch("::1") is None # ipv6 not supported 10 | assert sshuttle.server._ipmatch("42 Example Street, Melbourne") is None 11 | 12 | 13 | def test__ipstr(): 14 | assert sshuttle.server._ipstr("1.2.3.4", 24) == "1.2.3.4/24" 15 | assert sshuttle.server._ipstr("1.2.3.4", 32) == "1.2.3.4" 16 | 17 | 18 | def test__maskbits(): 19 | netmask = sshuttle.server._ipmatch("255.255.255.0") 20 | sshuttle.server._maskbits(netmask) 21 | 22 | 23 | @patch('sshuttle.server.which', side_effect=lambda x: x == 'netstat') 24 | @patch('sshuttle.server.ssubprocess.Popen') 25 | def test_listroutes_netstat(mock_popen, mock_which): 26 | mock_pobj = Mock() 27 | mock_pobj.stdout = io.BytesIO(b""" 28 | Kernel IP routing table 29 | Destination Gateway Genmask Flags MSS Window irtt Iface 30 | 0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 wlan0 31 | 192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 wlan0 32 | """) 33 | mock_pobj.wait.return_value = 0 34 | mock_popen.return_value = mock_pobj 35 | 36 | routes = sshuttle.server.list_routes() 37 | 38 | assert list(routes) == [ 39 | (socket.AF_INET, '192.168.1.0', 24) 40 | ] 41 | 42 | 43 | @patch('sshuttle.server.which', side_effect=lambda x: x == 'ip') 44 | @patch('sshuttle.server.ssubprocess.Popen') 45 | def test_listroutes_iproute(mock_popen, mock_which): 46 | mock_pobj = Mock() 47 | mock_pobj.stdout = io.BytesIO(b""" 48 | default via 192.168.1.1 dev wlan0 proto static 49 | 192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.1 50 | """) 51 | mock_pobj.wait.return_value = 0 52 | mock_popen.return_value = mock_pobj 53 | 54 | routes = sshuttle.server.list_routes() 55 | 56 | assert list(routes) == [ 57 | (socket.AF_INET, '192.168.1.0', 24) 58 | ] 59 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | downloadcache = {toxworkdir}/cache/ 3 | envlist = 4 | py27, 5 | py34, 6 | py35, 7 | py36, 8 | 9 | [testenv] 10 | basepython = 11 | py26: python2.6 12 | py27: python2.7 13 | py34: python3.4 14 | py35: python3.5 15 | py36: python3.6 16 | commands = 17 | flake8 sshuttle --count --select=E901,E999,F821,F822,F823 --show-source --statistics 18 | flake8 sshuttle --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 19 | py.test 20 | deps = 21 | -rrequirements-tests.txt 22 | --------------------------------------------------------------------------------