New Upstream Release - sshtunnel

Ready changes

Summary

Merged new upstream version: 0.4.0 (was: 0.1.4).

Resulting package

Built on 2023-01-06T15:41 (took 6m18s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-sshtunnel

Lintian Result

Diff

diff --git a/LICENSE b/LICENSE
index 3953212..58a509f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014-2016 Pahaz Blinov
+Copyright (c) 2014-2019 Pahaz White
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/MANIFEST.in b/MANIFEST.in
index 3eba37e..6b7ecac 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,9 +1,5 @@
 # Include the data files recursive-include data *
 
-# If using Python 2.6 or less, then have to include package data, even though
-# it's already declared in setup.py
-# include sample/*.dat
-
 include LICENSE
 include *.rst
 include docs/conf.py
@@ -11,3 +7,10 @@ include docs/Makefile
 include docs/*.rst
 include docs/*.txt
 include tests/*
+include e2e_tests/*
+include e2e_tests/ssh-server-config/*
+exclude .github/*
+exclude .circleci/*
+exclude *.pyc
+exclude __pycache__
+exclude Pipfile*
diff --git a/PKG-INFO b/PKG-INFO
index 6b2e89d..7f02d5c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,22 +1,21 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
 Name: sshtunnel
-Version: 0.1.4
+Version: 0.4.0
 Summary: Pure python SSH tunnels
 Home-page: https://github.com/pahaz/sshtunnel
-Author: Pahaz Blinov
-Author-email: pahaz.blinov@gmail.com
+Author: Pahaz White
+Author-email: pahaz.white@gmail.com
 License: MIT
-Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip
-Description-Content-Type: UNKNOWN
+Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.4.0.zip
 Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         
         |pyversions| |license|
         
-        **Author**: `Pahaz Blinov`_
+        **Author**: `Pahaz`_
         
         **Repo**: https://github.com/pahaz/sshtunnel/
         
-        Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on
+        Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on
         Windows.
         
         See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py
@@ -112,12 +111,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind
         port.
         
-        .. code-block:: py
+        .. code-block:: python
         
             from sshtunnel import SSHTunnelForwarder
         
             server = SSHTunnelForwarder(
-                'pahaz.urfuclub.ru',
+                'alfa.8iq.dev',
                 ssh_username="pahaz",
                 ssh_password="secret",
                 remote_bind_address=('127.0.0.1', 8080)
@@ -137,12 +136,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         assuming password protected pkey authentication, remote server's SSH service is
         listening on port 443 and that port is open in the firewall (**Fig2**):
         
-        .. code-block:: py
+        .. code-block:: python
         
             import paramiko
-            from sshtunnel import SSHTunnelForwarder
+            import sshtunnel
         
-            with SSHTunnelForwarder(
+            with sshtunnel.open_tunnel(
                 (REMOTE_SERVER_IP, 443),
                 ssh_username="",
                 ssh_pkey="/var/ssh/rsa_key",
@@ -164,12 +163,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         
         Example of a port forwarding for the Vagrant MySQL local port:
         
-        .. code-block:: py
+        .. code-block:: python
         
-            from sshtunnel import SSHTunnelForwarder
+            from sshtunnel import open_tunnel
             from time import sleep
         
-            with SSHTunnelForwarder(
+            with open_tunnel(
                 ('localhost', 2222),
                 ssh_username="vagrant",
                 ssh_password="vagrant",
@@ -189,6 +188,40 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         
             (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost
         
+        Example 4
+        ---------
+        
+        Opening an SSH session jumping over two tunnels. SSH transport and tunnels
+        will be daemonised, which will not wait for the connections to stop at close
+        time.
+        
+        .. code-block:: python
+        
+            import sshtunnel
+            from paramiko import SSHClient
+        
+        
+            with sshtunnel.open_tunnel(
+                ssh_address_or_host=('GW1_ip', 20022),
+                remote_bind_address=('GW2_ip', 22),
+            ) as tunnel1:
+                print('Connection to tunnel1 (GW1_ip:GW1_port) OK...')
+                with sshtunnel.open_tunnel(
+                    ssh_address_or_host=('localhost', tunnel1.local_bind_port),
+                    remote_bind_address=('target_ip', 22),
+                    ssh_username='GW2_user',
+                    ssh_password='GW2_pwd',
+                ) as tunnel2:
+                    print('Connection to tunnel2 (GW2_ip:GW2_port) OK...')
+                    with SSHClient() as ssh:
+                        ssh.connect('localhost',
+                            port=tunnel2.local_bind_port,
+                            username='target_user',
+                            password='target_pwd',
+                        )
+                        ssh.exec_command(...)
+        
+        
         CLI usage
         =========
         
@@ -198,11 +231,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
             usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R
                              IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]]
                              [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v]
-                             [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]]
+                             [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n]
+                             [-d [FOLDER [FOLDER ...]]]
                              ssh_address
         
             Pure python ssh tunnel utils
-            Version 0.1.4
+            Version 0.4.0
         
             positional arguments:
               ssh_address           SSH server IP address (GW for SSH tunnels)
@@ -246,7 +280,7 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
               -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]]
                                     List of directories where SSH pkeys (in the format `id_*`) may be found
         
-        .. _Pahaz Blinov: https://github.com/pahaz
+        .. _Pahaz: https://github.com/pahaz
         .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel
         .. _paramiko: http://www.paramiko.org/
         .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg
@@ -285,10 +319,45 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         - `Dan Harbin`_
         - `Ignacio Peluffo`_
         - `Niels Zeilemaker`_
+        - `Georgy Rylov`_
+        - `Eddie Chiang`_
+        - `kkrasovskii`_
         
         CHANGELOG
         =========
         
+        - v.0.4.0 (`Pahaz`_)
+            + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_)
+            + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_)
+            + Add docker based end to end hangs tests (`#219`_)
+        
+        - v.0.3.2 (`Pahaz`_, `JM Fernández`_)
+            + Fix host key directory detection
+            + Unify default ssh config folder to `~/.ssh`
+        
+        - v.0.3.1 (`Pahaz`_)
+            + Increase open connection timeout to 10 secods
+        
+        - v.0.3.0 (`Pahaz`_)
+            + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible)
+            + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible)
+            + Set transport keepalive to 5 second by default (disabled for version < 0.3.0)
+            + Set default transport timeout to 0.1
+            + Deprecate and remove `block_on_close` option
+            + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_)
+        
+        - v.0.2.2 (`Pahaz`_)
+            + Add `.stop(force=True)` for force close active connections (`#201`_)
+        
+        - v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_)
+            + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_)
+        
+        - v.0.2.0 (`Georgy Rylov`_)
+            + Support IPv6 without proxy command. Use built-in paramiko create socket logic. The logic tries to use ipv6 socket family first, then ipv4 socket family.
+        
+        - v.0.1.5 (`JM Fernández`_)
+            + Introduce `block_on_close` attribute
+        
         - v.0.1.4 (`Niels Zeilemaker`_)
             + Allow loading pkeys from `~/.ssh`
         
@@ -305,7 +374,7 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         - v.0.1.0 (`JM Fernández`_)
             + Add `tunnel_bindings` property
             + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...)
-              (`Pahaz Blinov`_, `JM Fernández`_)
+              (`Pahaz`_, `JM Fernández`_)
             + Add TRACE logging level (`JM Fernández`_)
             + Code and tests refactoring (`JM Fernández`_)
             + Drop python3.2 support
@@ -331,55 +400,56 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
             + Add coverage (`JM Fernández`_)
             + Refactoring (`JM Fernández`_)
         
-        - v.0.0.6 (`Pahaz Blinov`_)
-            + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_)
+        - v.0.0.6 (`Pahaz`_)
+            + add ``-S`` CLI options for ssh private key password support (`Pahaz`_)
         
-        - v.0.0.5 (`Pahaz Blinov`_)
+        - v.0.0.5 (`Pahaz`_)
             + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_)
             + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_)
             + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_)
             + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_)
-            + add tests (`Pahaz Blinov`_)
-            + add CI integration (`Pahaz Blinov`_)
-            + normal packaging (`Pahaz Blinov`_)
-            + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior]
-            + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior]
+            + add tests (`Pahaz`_)
+            + add CI integration (`Pahaz`_)
+            + normal packaging (`Pahaz`_)
+            + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior]
+            + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior]
         
-        - v.0.0.4.4 (`Pahaz Blinov`_)
-           + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_)
+        - v.0.0.4.4 (`Pahaz`_)
+           + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_)
         
-        - v.0.0.4.3 (`Pahaz Blinov`_)
-            + fix default port issue `#19`_ (`Pahaz Blinov`_)
+        - v.0.0.4.3 (`Pahaz`_)
+            + fix default port issue `#19`_ (`Pahaz`_)
         
-        - v.0.0.4.2 (`Pahaz Blinov`_)
+        - v.0.0.4.2 (`Pahaz`_)
             + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_)
         
-        - v.0.0.4.1 (`Pahaz Blinov`_)
-            + fix CLI issues `#13`_ (`Pahaz Blinov`_)
+        - v.0.0.4.1 (`Pahaz`_)
+            + fix CLI issues `#13`_ (`Pahaz`_)
         
-        - v.0.0.4 (`Pahaz Blinov`_)
-            + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible*
-            + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
-            + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
+        - v.0.0.4 (`Pahaz`_)
+            + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible*
+            + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
+            + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
             + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible*
             + add CLI interface (`JM Fernández`_)
             + support opening several tunnels at once (`JM Fernández`_)
-            + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_)
-            + improve logging (`JM Fernández`_, `Pahaz Blinov`_)
-            + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_)
+            + improve stability and readability (`JM Fernández`_, `Pahaz`_)
+            + improve logging (`JM Fernández`_, `Pahaz`_)
+            + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_)
             + add ``ssh_config_file`` argument support (`JM Fernández`_)
-            + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_)
+            + add Python 3 support (`JM Fernández`_, `Pahaz`_)
         
-        - v.0.0.3 (`Pahaz Blinov`_)
+        - v.0.0.3 (`Pahaz`_)
             + add ``threaded`` option (`Cameron Maske`_)
             + fix exception error message, correctly printing destination address (`Gustavo Machado`_)
-            + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_)
+            + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_)
         
-        - v.0.0.1 (`Pahaz Blinov`_)
-            + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_)
-            + ``open`` function (`Pahaz Blinov`_)
+        - v.0.0.1 (`Pahaz`_)
+            + ``SSHTunnelForwarder`` class (`Pahaz`_)
+            + ``open`` function (`Pahaz`_)
         
         
+        .. _Pahaz: https://github.com/pahaz
         .. _Cameron Maske: https://github.com/cameronmaske
         .. _Gustavo Machado: https://github.com/gdmachado
         .. _Colin Jermain: https://github.com/cjermain
@@ -391,6 +461,9 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         .. _Dan Harbin: https://github.com/RasterBurn
         .. _Ignacio Peluffo: https://github.com/ipeluffo
         .. _Niels Zeilemaker: https://github.com/NielsZeilemaker
+        .. _Georgy Rylov: https://github.com/g0djan
+        .. _Eddie Chiang: https://github.com/eddie-chiang
+        .. _kkrasovskii: https://github.com/kkrasovskii
         .. _#13: https://github.com/pahaz/sshtunnel/issues/13
         .. _#16: https://github.com/pahaz/sshtunnel/issues/16
         .. _#19: https://github.com/pahaz/sshtunnel/issues/19
@@ -404,6 +477,13 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         .. _#41: https://github.com/pahaz/sshtunnel/issues/41
         .. _#43: https://github.com/pahaz/sshtunnel/issues/43
         .. _#46: https://github.com/pahaz/sshtunnel/issues/46
+        .. _#170: https://github.com/pahaz/sshtunnel/issues/170
+        .. _#201: https://github.com/pahaz/sshtunnel/issues/201
+        .. _#162: https://github.com/pahaz/sshtunnel/issues/162
+        .. _#173: https://github.com/pahaz/sshtunnel/issues/173
+        .. _#201: https://github.com/pahaz/sshtunnel/issues/201
+        .. _#211: https://github.com/pahaz/sshtunnel/issues/211
+        .. _#219: https://github.com/pahaz/sshtunnel/issues/219
         .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127
         
 Keywords: ssh tunnel paramiko proxy tcp-forward
@@ -420,3 +500,9 @@ Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Description-Content-Type: text/x-rst
+Provides-Extra: dev
+Provides-Extra: test
+Provides-Extra: build_sphinx
diff --git a/README.rst b/README.rst
index 419cbae..65b04bd 100644
--- a/README.rst
+++ b/README.rst
@@ -2,11 +2,11 @@
 
 |pyversions| |license|
 
-**Author**: `Pahaz Blinov`_
+**Author**: `Pahaz`_
 
 **Repo**: https://github.com/pahaz/sshtunnel/
 
-Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on
+Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on
 Windows.
 
 See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py
@@ -102,12 +102,12 @@ Code corresponding to **Fig1** above follows, given remote server's address is
 ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind
 port.
 
-.. code-block:: py
+.. code-block:: python
 
     from sshtunnel import SSHTunnelForwarder
 
     server = SSHTunnelForwarder(
-        'pahaz.urfuclub.ru',
+        'alfa.8iq.dev',
         ssh_username="pahaz",
         ssh_password="secret",
         remote_bind_address=('127.0.0.1', 8080)
@@ -127,12 +127,12 @@ Example of a port forwarding to a private server not directly reachable,
 assuming password protected pkey authentication, remote server's SSH service is
 listening on port 443 and that port is open in the firewall (**Fig2**):
 
-.. code-block:: py
+.. code-block:: python
 
     import paramiko
-    from sshtunnel import SSHTunnelForwarder
+    import sshtunnel
 
-    with SSHTunnelForwarder(
+    with sshtunnel.open_tunnel(
         (REMOTE_SERVER_IP, 443),
         ssh_username="",
         ssh_pkey="/var/ssh/rsa_key",
@@ -154,12 +154,12 @@ Example 3
 
 Example of a port forwarding for the Vagrant MySQL local port:
 
-.. code-block:: py
+.. code-block:: python
 
-    from sshtunnel import SSHTunnelForwarder
+    from sshtunnel import open_tunnel
     from time import sleep
 
-    with SSHTunnelForwarder(
+    with open_tunnel(
         ('localhost', 2222),
         ssh_username="vagrant",
         ssh_password="vagrant",
@@ -179,6 +179,40 @@ Or simply using the CLI:
 
     (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost
 
+Example 4
+---------
+
+Opening an SSH session jumping over two tunnels. SSH transport and tunnels
+will be daemonised, which will not wait for the connections to stop at close
+time.
+
+.. code-block:: python
+
+    import sshtunnel
+    from paramiko import SSHClient
+
+
+    with sshtunnel.open_tunnel(
+        ssh_address_or_host=('GW1_ip', 20022),
+        remote_bind_address=('GW2_ip', 22),
+    ) as tunnel1:
+        print('Connection to tunnel1 (GW1_ip:GW1_port) OK...')
+        with sshtunnel.open_tunnel(
+            ssh_address_or_host=('localhost', tunnel1.local_bind_port),
+            remote_bind_address=('target_ip', 22),
+            ssh_username='GW2_user',
+            ssh_password='GW2_pwd',
+        ) as tunnel2:
+            print('Connection to tunnel2 (GW2_ip:GW2_port) OK...')
+            with SSHClient() as ssh:
+                ssh.connect('localhost',
+                    port=tunnel2.local_bind_port,
+                    username='target_user',
+                    password='target_pwd',
+                )
+                ssh.exec_command(...)
+
+
 CLI usage
 =========
 
@@ -188,11 +222,12 @@ CLI usage
     usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R
                      IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]]
                      [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v]
-                     [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]]
+                     [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n]
+                     [-d [FOLDER [FOLDER ...]]]
                      ssh_address
 
     Pure python ssh tunnel utils
-    Version 0.1.4
+    Version 0.4.0
 
     positional arguments:
       ssh_address           SSH server IP address (GW for SSH tunnels)
@@ -236,7 +271,7 @@ CLI usage
       -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]]
                             List of directories where SSH pkeys (in the format `id_*`) may be found
 
-.. _Pahaz Blinov: https://github.com/pahaz
+.. _Pahaz: https://github.com/pahaz
 .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel
 .. _paramiko: http://www.paramiko.org/
 .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg
diff --git a/changelog.rst b/changelog.rst
index 6d877bc..4ace59f 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -12,10 +12,45 @@ CONTRIBUTORS
 - `Dan Harbin`_
 - `Ignacio Peluffo`_
 - `Niels Zeilemaker`_
+- `Georgy Rylov`_
+- `Eddie Chiang`_
+- `kkrasovskii`_
 
 CHANGELOG
 =========
 
+- v.0.4.0 (`Pahaz`_)
+    + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_)
+    + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_)
+    + Add docker based end to end hangs tests (`#219`_)
+
+- v.0.3.2 (`Pahaz`_, `JM Fernández`_)
+    + Fix host key directory detection
+    + Unify default ssh config folder to `~/.ssh`
+
+- v.0.3.1 (`Pahaz`_)
+    + Increase open connection timeout to 10 secods
+
+- v.0.3.0 (`Pahaz`_)
+    + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible)
+    + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible)
+    + Set transport keepalive to 5 second by default (disabled for version < 0.3.0)
+    + Set default transport timeout to 0.1
+    + Deprecate and remove `block_on_close` option
+    + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_)
+
+- v.0.2.2 (`Pahaz`_)
+    + Add `.stop(force=True)` for force close active connections (`#201`_)
+
+- v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_)
+    + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_)
+
+- v.0.2.0 (`Georgy Rylov`_)
+    + Support IPv6 without proxy command. Use built-in paramiko create socket logic. The logic tries to use ipv6 socket family first, then ipv4 socket family.
+
+- v.0.1.5 (`JM Fernández`_)
+    + Introduce `block_on_close` attribute
+
 - v.0.1.4 (`Niels Zeilemaker`_)
     + Allow loading pkeys from `~/.ssh`
 
@@ -32,7 +67,7 @@ CHANGELOG
 - v.0.1.0 (`JM Fernández`_)
     + Add `tunnel_bindings` property
     + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...)
-      (`Pahaz Blinov`_, `JM Fernández`_)
+      (`Pahaz`_, `JM Fernández`_)
     + Add TRACE logging level (`JM Fernández`_)
     + Code and tests refactoring (`JM Fernández`_)
     + Drop python3.2 support
@@ -58,55 +93,56 @@ CHANGELOG
     + Add coverage (`JM Fernández`_)
     + Refactoring (`JM Fernández`_)
 
-- v.0.0.6 (`Pahaz Blinov`_)
-    + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_)
+- v.0.0.6 (`Pahaz`_)
+    + add ``-S`` CLI options for ssh private key password support (`Pahaz`_)
 
-- v.0.0.5 (`Pahaz Blinov`_)
+- v.0.0.5 (`Pahaz`_)
     + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_)
     + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_)
     + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_)
     + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_)
-    + add tests (`Pahaz Blinov`_)
-    + add CI integration (`Pahaz Blinov`_)
-    + normal packaging (`Pahaz Blinov`_)
-    + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior]
-    + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior]
+    + add tests (`Pahaz`_)
+    + add CI integration (`Pahaz`_)
+    + normal packaging (`Pahaz`_)
+    + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior]
+    + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior]
 
-- v.0.0.4.4 (`Pahaz Blinov`_)
-   + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_)
+- v.0.0.4.4 (`Pahaz`_)
+   + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_)
 
-- v.0.0.4.3 (`Pahaz Blinov`_)
-    + fix default port issue `#19`_ (`Pahaz Blinov`_)
+- v.0.0.4.3 (`Pahaz`_)
+    + fix default port issue `#19`_ (`Pahaz`_)
 
-- v.0.0.4.2 (`Pahaz Blinov`_)
+- v.0.0.4.2 (`Pahaz`_)
     + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_)
 
-- v.0.0.4.1 (`Pahaz Blinov`_)
-    + fix CLI issues `#13`_ (`Pahaz Blinov`_)
+- v.0.0.4.1 (`Pahaz`_)
+    + fix CLI issues `#13`_ (`Pahaz`_)
 
-- v.0.0.4 (`Pahaz Blinov`_)
-    + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible*
-    + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
-    + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
+- v.0.0.4 (`Pahaz`_)
+    + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible*
+    + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
+    + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
     + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible*
     + add CLI interface (`JM Fernández`_)
     + support opening several tunnels at once (`JM Fernández`_)
-    + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_)
-    + improve logging (`JM Fernández`_, `Pahaz Blinov`_)
-    + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_)
+    + improve stability and readability (`JM Fernández`_, `Pahaz`_)
+    + improve logging (`JM Fernández`_, `Pahaz`_)
+    + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_)
     + add ``ssh_config_file`` argument support (`JM Fernández`_)
-    + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_)
+    + add Python 3 support (`JM Fernández`_, `Pahaz`_)
 
-- v.0.0.3 (`Pahaz Blinov`_)
+- v.0.0.3 (`Pahaz`_)
     + add ``threaded`` option (`Cameron Maske`_)
     + fix exception error message, correctly printing destination address (`Gustavo Machado`_)
-    + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_)
+    + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_)
 
-- v.0.0.1 (`Pahaz Blinov`_)
-    + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_)
-    + ``open`` function (`Pahaz Blinov`_)
+- v.0.0.1 (`Pahaz`_)
+    + ``SSHTunnelForwarder`` class (`Pahaz`_)
+    + ``open`` function (`Pahaz`_)
 
 
+.. _Pahaz: https://github.com/pahaz
 .. _Cameron Maske: https://github.com/cameronmaske
 .. _Gustavo Machado: https://github.com/gdmachado
 .. _Colin Jermain: https://github.com/cjermain
@@ -118,6 +154,9 @@ CHANGELOG
 .. _Dan Harbin: https://github.com/RasterBurn
 .. _Ignacio Peluffo: https://github.com/ipeluffo
 .. _Niels Zeilemaker: https://github.com/NielsZeilemaker
+.. _Georgy Rylov: https://github.com/g0djan
+.. _Eddie Chiang: https://github.com/eddie-chiang
+.. _kkrasovskii: https://github.com/kkrasovskii
 .. _#13: https://github.com/pahaz/sshtunnel/issues/13
 .. _#16: https://github.com/pahaz/sshtunnel/issues/16
 .. _#19: https://github.com/pahaz/sshtunnel/issues/19
@@ -131,4 +170,11 @@ CHANGELOG
 .. _#41: https://github.com/pahaz/sshtunnel/issues/41
 .. _#43: https://github.com/pahaz/sshtunnel/issues/43
 .. _#46: https://github.com/pahaz/sshtunnel/issues/46
+.. _#170: https://github.com/pahaz/sshtunnel/issues/170
+.. _#201: https://github.com/pahaz/sshtunnel/issues/201
+.. _#162: https://github.com/pahaz/sshtunnel/issues/162
+.. _#173: https://github.com/pahaz/sshtunnel/issues/173
+.. _#201: https://github.com/pahaz/sshtunnel/issues/201
+.. _#211: https://github.com/pahaz/sshtunnel/issues/211
+.. _#219: https://github.com/pahaz/sshtunnel/issues/219
 .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127
diff --git a/debian/changelog b/debian/changelog
index 69b7d3e..5f8db51 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,10 @@
-sshtunnel (0.1.4-4) UNRELEASED; urgency=medium
+sshtunnel (0.4.0-1) UNRELEASED; urgency=medium
 
   * Bump debhelper from old 12 to 13.
   * Update standards version to 4.1.5, no changes needed.
+  * New upstream release.
 
- -- Debian Janitor <janitor@jelmer.uk>  Fri, 06 Jan 2023 09:34:08 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 06 Jan 2023 15:35:41 -0000
 
 sshtunnel (0.1.4-3) unstable; urgency=medium
 
diff --git a/docs/conf.py b/docs/conf.py
index e3bd5b3..7be950a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,6 +16,8 @@
 import sys
 import os
 
+import sshtunnel
+
 # Patch to disable warning on non-local image
 import sphinx.environment
 from docutils.utils import get_source_line
@@ -61,17 +63,17 @@ master_doc = 'index'
 
 # General information about the project.
 project = 'sshtunnel'
-copyright = '2014-2016, Pahaz Blinov and contributors'
-author = 'Pahaz Blinov'
+copyright = '2014-2020, Pahaz White and contributors'
+author = 'Pahaz White'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.0.8'
+version = sshtunnel.__version__
 # The full version, including alpha/beta/rc tags.
-release = '0.0.8'
+release = sshtunnel.__version__
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/index.rst b/docs/index.rst
index 4210823..2e38cda 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -8,9 +8,6 @@ Welcome to sshtunnel's documentation!
 API
 ===
 
-.. toctree::
-   :maxdepth: 3
-
 .. automodule:: sshtunnel
    :members:
    :member-order: bysource
@@ -21,4 +18,4 @@ API
 License
 =======
 
-.. include:: ../LICENSE
\ No newline at end of file
+.. include:: ../LICENSE
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
deleted file mode 100644
index 1d3c833..0000000
--- a/docs/requirements-docs.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-docutils==0.12
-sphinx==1.3.5
-sphinxcontrib-napoleon==0.5.0
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..76356ef
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,3 @@
+docutils
+sphinx
+sphinxcontrib-napoleon
diff --git a/e2e_tests/docker-compose.yaml b/e2e_tests/docker-compose.yaml
new file mode 100644
index 0000000..5f559c8
--- /dev/null
+++ b/e2e_tests/docker-compose.yaml
@@ -0,0 +1,60 @@
+---
+version: "2.1"
+services:
+  ssh:
+    image: ghcr.io/linuxserver/openssh-server
+    container_name: openssh-server
+    hostname: openssh-server
+    environment:
+      - PUID=1000
+      - PGID=1000
+      - TZ=Europe/London
+      - PUBLIC_KEY_FILE=/config/ssh_host_keys/ssh_host_rsa_key.pub
+      - SUDO_ACCESS=false
+      - PASSWORD_ACCESS=false
+      - USER_NAME=linuxserver
+    volumes:
+      - ./ssh-server-config:/config/ssh_host_keys:ro
+    ports:
+      - "127.0.0.1:2223:2222"
+    networks:
+      - inner
+
+  postgresdb:
+    image: postgres:13.0
+    environment:
+      POSTGRES_USER: postgres
+      POSTGRES_PASSWORD: postgres
+      POSTGRES_DB: main
+    networks:
+      inner:
+        ipv4_address: 10.5.0.5
+
+  mysqldb:
+    image: mysql:8
+    environment:
+      MYSQL_DATABASE: main
+      MYSQL_USER: mysql
+      MYSQL_PASSWORD: mysql
+      MYSQL_ROOT_PASSWORD: mysqlroot
+    networks:
+      inner:
+        ipv4_address: 10.5.0.6
+
+  mongodb:
+    image: mongo:3.6
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: mongo
+      MONGO_INITDB_ROOT_PASSWORD: mongo
+      MONGO_INITDB_DATABASE: main
+    networks:
+      inner:
+        ipv4_address: 10.5.0.7
+
+networks:
+  inner:
+    driver: bridge
+    ipam:
+     config:
+       - subnet: 10.5.0.0/16
+         gateway: 10.5.0.1
diff --git a/e2e_tests/run_docker_e2e_db_tests.py b/e2e_tests/run_docker_e2e_db_tests.py
new file mode 100644
index 0000000..e14edd3
--- /dev/null
+++ b/e2e_tests/run_docker_e2e_db_tests.py
@@ -0,0 +1,241 @@
+import select
+import traceback
+import sys
+import os
+import time
+from sshtunnel import SSHTunnelForwarder
+import sshtunnel
+import logging
+import threading
+import paramiko
+
+sshtunnel.DEFAULT_LOGLEVEL = 1
+logging.basicConfig(
+    format='%(asctime)s| %(levelname)-4.3s|%(threadName)10.9s/%(lineno)04d@%(module)-10.9s| %(message)s', level=1)
+
+SSH_SERVER_ADDRESS = ('127.0.0.1', 2223)
+SSH_SERVER_USERNAME = 'linuxserver'
+SSH_PKEY = os.path.join(os.path.dirname(__file__), 'ssh-server-config', 'ssh_host_rsa_key')
+SSH_SERVER_REMOTE_SIDE_ADDRESS_PG = ('10.5.0.5', 5432)
+SSH_SERVER_REMOTE_SIDE_ADDRESS_MYSQL = ('10.5.0.6', 3306)
+SSH_SERVER_REMOTE_SIDE_ADDRESS_MONGO = ('10.5.0.7', 27017)
+
+PG_DATABASE_NAME = 'main'
+PG_USERNAME = 'postgres'
+PG_PASSWORD = 'postgres'
+PG_QUERY = 'select version()'
+PG_EXPECT = eval(
+    """('PostgreSQL 13.0 (Debian 13.0-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit',)""")
+
+MYSQL_DATABASE_NAME = 'main'
+MYSQL_USERNAME = 'mysql'
+MYSQL_PASSWORD = 'mysql'
+MYSQL_QUERY = 'select version()'
+MYSQL_EXPECT = (('8.0.22',),)
+
+MONGO_DATABASE_NAME = 'main'
+MONGO_USERNAME = 'mongo'
+MONGO_PASSWORD = 'mongo'
+MONGO_QUERY = lambda client, db: client.server_info()
+MONGO_EXPECT = eval(
+    """{'version': '3.6.21', 'gitVersion': '1cd2db51dce4b16f4bc97a75056269df0dc0bddb', 'modules': [], 'allocator': 'tcmalloc', 'javascriptEngine': 'mozjs', 'sysInfo': 'deprecated', 'versionArray': [3, 6, 21, 0], 'openssl': {'running': 'OpenSSL 1.0.2g  1 Mar 2016', 'compiled': 'OpenSSL 1.0.2g  1 Mar 2016'}, 'buildEnvironment': {'distmod': 'ubuntu1604', 'distarch': 'x86_64', 'cc': '/opt/mongodbtoolchain/v2/bin/gcc: gcc (GCC) 5.4.0', 'ccflags': '-fno-omit-frame-pointer -fno-strict-aliasing -ggdb -pthread -Wall -Wsign-compare -Wno-unknown-pragmas -Winvalid-pch -Werror -O2 -Wno-unused-local-typedefs -Wno-unused-function -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-missing-braces -fstack-protector-strong -fno-builtin-memcmp', 'cxx': '/opt/mongodbtoolchain/v2/bin/g++: g++ (GCC) 5.4.0', 'cxxflags': '-Woverloaded-virtual -Wno-maybe-uninitialized -std=c++14', 'linkflags': '-pthread -Wl,-z,now -rdynamic -Wl,--fatal-warnings -fstack-protector-strong -fuse-ld=gold -Wl,--build-id -Wl,--hash-style=gnu -Wl,-z,noexecstack -Wl,--warn-execstack -Wl,-z,relro', 'target_arch': 'x86_64', 'target_os': 'linux'}, 'bits': 64, 'debug': False, 'maxBsonObjectSize': 16777216, 'storageEngines': ['devnull', 'ephemeralForTest', 'mmapv1', 'wiredTiger'], 'ok': 1.0}""")
+
+
+def run_postgres_query(port, query=PG_QUERY):
+    import psycopg2
+
+    ASYNC_OK = 1
+    ASYNC_READ_TIMEOUT = 2
+    ASYNC_WRITE_TIMEOUT = 3
+    ASYNC_TIMEOUT = 0.2
+
+    def wait(conn):
+        while 1:
+            state = conn.poll()
+            if state == psycopg2.extensions.POLL_OK:
+                break
+            elif state == psycopg2.extensions.POLL_WRITE:
+                select.select([], [conn.fileno()], [])
+            elif state == psycopg2.extensions.POLL_READ:
+                select.select([conn.fileno()], [], [])
+            else:
+                raise psycopg2.OperationalError(
+                    "poll() returned %s from _wait function" % state)
+
+    def wait_timeout(conn):
+        while 1:
+            state = conn.poll()
+            if state == psycopg2.extensions.POLL_OK:
+                return ASYNC_OK
+            elif state == psycopg2.extensions.POLL_WRITE:
+                # Wait for the given time and then check the return status
+                # If three empty lists are returned then the time-out is
+                # reached.
+                timeout_status = select.select(
+                    [], [conn.fileno()], [], ASYNC_TIMEOUT
+                )
+                if timeout_status == ([], [], []):
+                    return ASYNC_WRITE_TIMEOUT
+            elif state == psycopg2.extensions.POLL_READ:
+                # Wait for the given time and then check the return status
+                # If three empty lists are returned then the time-out is
+                # reached.
+                timeout_status = select.select(
+                    [conn.fileno()], [], [], ASYNC_TIMEOUT
+                )
+                if timeout_status == ([], [], []):
+                    return ASYNC_READ_TIMEOUT
+            else:
+                raise psycopg2.OperationalError(
+                    "poll() returned %s from _wait_timeout function" % state
+                )
+
+    pg_conn = psycopg2.connect(
+        host='127.0.0.1',
+        hostaddr='127.0.0.1',
+        port=port,
+        database=PG_DATABASE_NAME,
+        user=PG_USERNAME,
+        password=PG_PASSWORD,
+        sslmode='disable',
+        async_=1
+    )
+    wait(pg_conn)
+    cur = pg_conn.cursor()
+    cur.execute(query)
+    res = wait_timeout(cur.connection)
+    while res != ASYNC_OK:
+        res = wait_timeout(cur.connection)
+    return cur.fetchone()
+
+
+def run_mysql_query(port, query=MYSQL_QUERY):
+    import pymysql
+    conn = pymysql.connect(
+        host='127.0.0.1',
+        port=port,
+        user=MYSQL_USERNAME,
+        password=MYSQL_PASSWORD,
+        database=MYSQL_DATABASE_NAME,
+        connect_timeout=5,
+        read_timeout=5)
+    cursor = conn.cursor()
+    cursor.execute(query)
+    return cursor.fetchall()
+
+
+def run_mongo_query(port, query=MONGO_QUERY):
+    import pymongo
+    client = pymongo.MongoClient('127.0.0.1', port)
+    db = client[MONGO_DATABASE_NAME]
+    return query(client, db)
+
+
+def create_tunnel():
+    logging.info('Creating SSHTunnelForwarder... (sshtunnel v%s, paramiko v%s)',
+                 sshtunnel.__version__, paramiko.__version__)
+    tunnel = SSHTunnelForwarder(
+        SSH_SERVER_ADDRESS,
+        ssh_username=SSH_SERVER_USERNAME,
+        ssh_pkey=SSH_PKEY,
+        remote_bind_addresses=[
+            SSH_SERVER_REMOTE_SIDE_ADDRESS_PG, SSH_SERVER_REMOTE_SIDE_ADDRESS_MYSQL,
+            SSH_SERVER_REMOTE_SIDE_ADDRESS_MONGO,
+        ],
+    )
+    return tunnel
+
+
+def start(tunnel):
+    try:
+        logging.info('Trying to start ssh tunnel...')
+        tunnel.start()
+    except Exception as e:
+        logging.exception('Tunnel start exception: %r', e)
+        raise
+
+
+def run_db_queries(tunnel):
+    result1, result2, result3 = None, None, None
+
+    try:
+        logging.info('Trying to run PG query...')
+        result1 = run_postgres_query(tunnel.local_bind_ports[0])
+        logging.info('PG query: %r', result1)
+    except Exception as e:
+        logging.exception('PG query exception: %r', e)
+        raise
+
+    try:
+        logging.info('Trying to run MYSQL query...')
+        result2 = run_mysql_query(tunnel.local_bind_ports[1])
+        logging.info('MYSQL query: %r', result2)
+    except Exception as e:
+        logging.exception('MYSQL query exception: %r', e)
+        raise
+
+    try:
+        logging.info('Trying to run MONGO query...')
+        result3 = run_mongo_query(tunnel.local_bind_ports[2])
+        logging.info('MONGO query: %r', result3)
+    except Exception as e:
+        logging.exception('MONGO query exception: %r', e)
+        raise
+
+    return result1, result2, result3
+
+
+def wait_and_check_or_restart_if_required(tunnel, i=1):
+    logging.warning('Sleeping for %s second...', i)
+    while i:
+        time.sleep(1)
+        if i % 10 == 0:
+            logging.info('Running tunnel.check_tunnels... (i=%s)', i)
+            tunnel.check_tunnels()
+            logging.info('Check result: %r (i=%s)', tunnel.tunnel_is_up, i)
+            if not tunnel.is_active:
+                logging.warning('Tunnel is DOWN! restarting ...')
+                tunnel.restart()
+        i -= 1
+
+
+def stop(tunnel, force=True):
+    try:
+        logging.info('Trying to stop resources...')
+        tunnel.stop(force=force)
+    except Exception as e:
+        logging.exception('Tunnel stop exception: %r', e)
+        raise
+
+
+def show_threading_state_if_required():
+    current_threads = list(threading.enumerate())
+    if len(current_threads) > 1:
+        logging.warning('[1] THREAD INFO')
+        logging.info('Threads: %r', current_threads)
+        logging.info('Threads.daemon: %r', [x.daemon for x in current_threads])
+
+    if len(current_threads) > 1:
+        logging.warning('[2] STACK INFO')
+        code = ["\n\n*** STACKTRACE - START ***\n"]
+        for threadId, stack in sys._current_frames().items():
+            code.append("\n# ThreadID: %s" % threadId)
+            for filename, lineno, name, line in traceback.extract_stack(stack):
+                code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
+                if line:
+                    code.append("  %s" % (line.strip()))
+        code.append("\n*** STACKTRACE - END ***\n\n")
+        logging.info('\n'.join(code))
+
+
+if __name__ == '__main__':
+    logging.warning('RUN')
+    tunnel = create_tunnel()
+    start(tunnel)
+    res = run_db_queries(tunnel)
+    stop(tunnel)
+    wait_and_check_or_restart_if_required(tunnel)
+    show_threading_state_if_required()
+    logging.warning('EOF')
+
+    assert res == (PG_EXPECT, MYSQL_EXPECT, MONGO_EXPECT)
diff --git a/e2e_tests/run_docker_e2e_hangs_tests.py b/e2e_tests/run_docker_e2e_hangs_tests.py
new file mode 100644
index 0000000..0ec7449
--- /dev/null
+++ b/e2e_tests/run_docker_e2e_hangs_tests.py
@@ -0,0 +1,13 @@
+import logging
+import sshtunnel
+import os
+
+
+if __name__ == '__main__':
+    path = os.path.join(os.path.dirname(__file__), 'run_docker_e2e_db_tests.py')
+    with open(path) as f:
+        exec(f.read())
+    logging.warning('RUN')
+    tunnel = create_tunnel()
+    start(tunnel)
+    logging.warning('EOF')
diff --git a/e2e_tests/ssh-server-config/ssh_host_dsa_key b/e2e_tests/ssh-server-config/ssh_host_dsa_key
new file mode 100644
index 0000000..3c7e9af
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_dsa_key
@@ -0,0 +1,21 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH
+NzAAAAgQDSZjQVKBCj57wXTZTFusc/Amp5wet2ugo/Mh+86+v2WDbluFztNZXTA3EtX8p6
+zZtoLZJ/+VCtLqZD7MjJIt4/bPhOjyXOlbtIwL7w80drTxMFBOvuBQkD+TqIzaONwzsN5b
+GcQNACpyz4C2eSUP4KOmOrKXovFI6pMQ22lbqrrQAAABUAv4qw6qJkET1T4J8o0RgzoxNI
+TFkAAACAQQ5w7+2rPlC/GP9ScUCQZTicgzAYlTNOCvIcO4pRj7E1NwNMuafl6xNRjrIYBp
+OqMhDLIBx15Yob0J/6PpE65oeQ8Lq8QboZxO8bio0FGt4qE6mXB4vJq2oOwQkWHzH64x9l
+fmFQNe8KRpd0G/daXBgeF+FEqV2vVsjsjKXxwncAAACAFRvMwvnkzX/c2MaWx78+HJEjjf
+ATYt2acoLAH2YRwnhavQyEScNQDiZnBbIr2J21ccvGvFyZT2dtcz83pwFDa9o7Y41EWQG7
+ifRPYrj9aHd3TyxeiSGSZlna9ekcfXbIF7+aRHSyEie/YIYUGm73jCW+TDcXK1nQHu7tGL
+1KkBQAAAHox++oGsfvqBoAAAAHc3NoLWRzcwAAAIEA0mY0FSgQo+e8F02UxbrHPwJqecHr
+droKPzIfvOvr9lg25bhc7TWV0wNxLV/Kes2baC2Sf/lQrS6mQ+zIySLeP2z4To8lzpW7SM
+C+8PNHa08TBQTr7gUJA/k6iM2jjcM7DeWxnEDQAqcs+AtnklD+Cjpjqyl6LxSOqTENtpW6
+q60AAAAVAL+KsOqiZBE9U+CfKNEYM6MTSExZAAAAgEEOcO/tqz5Qvxj/UnFAkGU4nIMwGJ
+UzTgryHDuKUY+xNTcDTLmn5esTUY6yGAaTqjIQyyAcdeWKG9Cf+j6ROuaHkPC6vEG6GcTv
+G4qNBRreKhOplweLyatqDsEJFh8x+uMfZX5hUDXvCkaXdBv3WlwYHhfhRKldr1bI7Iyl8c
+J3AAAAgBUbzML55M1/3NjGlse/PhyRI43wE2LdmnKCwB9mEcJ4Wr0MhEnDUA4mZwWyK9id
+tXHLxrxcmU9nbXM/N6cBQ2vaO2ONRFkBu4n0T2K4/Wh3d08sXokhkmZZ2vXpHH12yBe/mk
+R0shInv2CGFBpu94wlvkw3FytZ0B7u7Ri9SpAUAAAAFAZscEj14jPPE+Znbk4FflEe6t2r
+AAAAE3Jvb3RAb3BlbnNzaC1zZXJ2ZXI=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/e2e_tests/ssh-server-config/ssh_host_dsa_key.pub b/e2e_tests/ssh-server-config/ssh_host_dsa_key.pub
new file mode 100644
index 0000000..7fea8e5
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_dsa_key.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBANJmNBUoEKPnvBdNlMW6xz8CannB63a6Cj8yH7zr6/ZYNuW4XO01ldMDcS1fynrNm2gtkn/5UK0upkPsyMki3j9s+E6PJc6Vu0jAvvDzR2tPEwUE6+4FCQP5OojNo43DOw3lsZxA0AKnLPgLZ5JQ/go6Y6spei8UjqkxDbaVuqutAAAAFQC/irDqomQRPVPgnyjRGDOjE0hMWQAAAIBBDnDv7as+UL8Y/1JxQJBlOJyDMBiVM04K8hw7ilGPsTU3A0y5p+XrE1GOshgGk6oyEMsgHHXlihvQn/o+kTrmh5DwurxBuhnE7xuKjQUa3ioTqZcHi8mrag7BCRYfMfrjH2V+YVA17wpGl3Qb91pcGB4X4USpXa9WyOyMpfHCdwAAAIAVG8zC+eTNf9zYxpbHvz4ckSON8BNi3ZpygsAfZhHCeFq9DIRJw1AOJmcFsivYnbVxy8a8XJlPZ21zPzenAUNr2jtjjURZAbuJ9E9iuP1od3dPLF6JIZJmWdr16Rx9dsgXv5pEdLISJ79ghhQabveMJb5MNxcrWdAe7u0YvUqQFA== root@openssh-server
diff --git a/e2e_tests/ssh-server-config/ssh_host_ecdsa_key b/e2e_tests/ssh-server-config/ssh_host_ecdsa_key
new file mode 100644
index 0000000..b94696b
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_ecdsa_key
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS9T8ajwjHV1Xl705ShFqry77rS7wrh
+lCN0a4Hf33yapuCBKCRDr+/Y7gISoiER3rez56TQCvIFuKUEgCUsTMSWAAAAsFXWKa1V1i
+mtAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL1PxqPCMdXVeXvT
+lKEWqvLvutLvCuGUI3Rrgd/ffJqm4IEoJEOv79juAhKiIRHet7PnpNAK8gW4pQSAJSxMxJ
+YAAAAhAOe2L6cmoGh4gZ++o+GiqMQ2WQ3RUfle/gc0G1nhLPhWAAAAE3Jvb3RAb3BlbnNz
+aC1zZXJ2ZXIBAgME
+-----END OPENSSH PRIVATE KEY-----
diff --git a/e2e_tests/ssh-server-config/ssh_host_ecdsa_key.pub b/e2e_tests/ssh-server-config/ssh_host_ecdsa_key.pub
new file mode 100644
index 0000000..bb68789
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_ecdsa_key.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL1PxqPCMdXVeXvTlKEWqvLvutLvCuGUI3Rrgd/ffJqm4IEoJEOv79juAhKiIRHet7PnpNAK8gW4pQSAJSxMxJY= root@openssh-server
diff --git a/e2e_tests/ssh-server-config/ssh_host_ed25519_key b/e2e_tests/ssh-server-config/ssh_host_ed25519_key
new file mode 100644
index 0000000..a99b456
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_ed25519_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCjD/qc+T3Cc/k2pbjOUFW0OobeLnoWUAoaBhTQchI26wAAAJgismUyIrJl
+MgAAAAtzc2gtZWQyNTUxOQAAACCjD/qc+T3Cc/k2pbjOUFW0OobeLnoWUAoaBhTQchI26w
+AAAECV2axXFduGtP3RS1f97smocwVLphmETzpWdwi89jWrJaMP+pz5PcJz+TaluM5QVbQ6
+ht4uehZQChoGFNByEjbrAAAAE3Jvb3RAb3BlbnNzaC1zZXJ2ZXIBAg==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/e2e_tests/ssh-server-config/ssh_host_ed25519_key.pub b/e2e_tests/ssh-server-config/ssh_host_ed25519_key.pub
new file mode 100644
index 0000000..b07edee
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_ed25519_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKMP+pz5PcJz+TaluM5QVbQ6ht4uehZQChoGFNByEjbr root@openssh-server
diff --git a/e2e_tests/ssh-server-config/ssh_host_rsa_key b/e2e_tests/ssh-server-config/ssh_host_rsa_key
new file mode 100644
index 0000000..01f74b4
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_rsa_key
@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAYEAvIGU0pRpThhIcaSPrg2+v7cXl+QcG0icb45hfD44yrCoXkpJp7nh
+Hv0ObZL2Y1cG7eeayYF4AqD3kwQ7W89GN6YO9b/mkJgawk0/YLUyojTS9dbcTbdkfPzyUa
+vTMDjly+PIjfiWOEnUgPf1y3xONLkJU0ILyTmgTzSIMNdKngtdCGfytBCuNiPKU8hEdEVt
+82ebqgtLoSYn9cUcVVz6LewzUh8+YtoPb8Z/BIVEzU37HiE9MOYIBXjo1AEJSnOCkjwlVl
+PzLhcXKTPht0iwv/KnZNNg0LDmnU/z0n+nPq/EMflum8jRYbgp0C5hksPdc8e0eEKd9gak
+t7B0ta3Mjt5b8HPQdBGZI/QFufEnSOxfJmoK4Bvjy/oUwE0hGU6po5g+4T2j6Bqqm2I+yV
+EbkP/UiuD/kEiT0C3yCV547gIDjN2ME9tGJDkd023BFvqn3stFVVZ5WsisRKGc+lvTfqeA
+JyKFaVt5a23y68ztjEMVrMLksRuEF8gG5kV7EGyjAAAFiCzGBRksxgUZAAAAB3NzaC1yc2
+EAAAGBALyBlNKUaU4YSHGkj64Nvr+3F5fkHBtInG+OYXw+OMqwqF5KSae54R79Dm2S9mNX
+Bu3nmsmBeAKg95MEO1vPRjemDvW/5pCYGsJNP2C1MqI00vXW3E23ZHz88lGr0zA45cvjyI
+34ljhJ1ID39ct8TjS5CVNCC8k5oE80iDDXSp4LXQhn8rQQrjYjylPIRHRFbfNnm6oLS6Em
+J/XFHFVc+i3sM1IfPmLaD2/GfwSFRM1N+x4hPTDmCAV46NQBCUpzgpI8JVZT8y4XFykz4b
+dIsL/yp2TTYNCw5p1P89J/pz6vxDH5bpvI0WG4KdAuYZLD3XPHtHhCnfYGpLewdLWtzI7e
+W/Bz0HQRmSP0BbnxJ0jsXyZqCuAb48v6FMBNIRlOqaOYPuE9o+gaqptiPslRG5D/1Irg/5
+BIk9At8gleeO4CA4zdjBPbRiQ5HdNtwRb6p97LRVVWeVrIrEShnPpb036ngCcihWlbeWtt
+8uvM7YxDFazC5LEbhBfIBuZFexBsowAAAAMBAAEAAAGAflHjdb2oV4HkQetBsSRa18QM1m
+cxAoOE+SiTYRudGQ6KtSzY8MGZ/xca7QiXfXhbF1+llTTiQ/i0Dtu+H0blyfLIgZwIGIsl
+G2GCf/7MoG//kmhaFuY3O56Rj3MyQVVPgHLy+VhE6hFniske+C4jhicc/aL7nOu15n3Qad
+JLmV8KB9EIjevDoloXgk9ot/WyuXKLmMaa9rFIA+UDmJyGtfFbbsOrHbj8sS11/oSD14RT
+LBygEb2EUI52j2LmY/LEvUL+59oCuJ6Y/h+pMdFeuHJzGjrVb573KnGwejzY24HHzzebrC
+Q+9NyVCTyizPHNu9w52/GPEZQFQBi7o9cDMd3ITZEPIaIvDHsUwPXaHUBHy/XHQTs8pDqk
+zCMcAs5zdzao2I0LQ+ZFYyvl1rue82ITjDISX1WK6nFYLBVXugi0rLGEdH6P+Psfl3uCIf
+aW7c12/BpZz2Pql5AuO1wsu4rmz2th68vaC/0IDqWekIbW9qihFbqnhfAxRsIURjpBAAAA
+wDhIQPsj9T9Vud3Z/TZjiAKCPbg3zi082u1GMMxXnNQtKO3J35wU7VUcAxAzosWr+emMqS
+U0qW+a5RXr3sqUOqH85b5+Xw0yv2sTr2pL0ALFW7Tq1mesCc3K0So3Yo30pWRIOxYM9ihm
+E4ci/3mN5kcKWwvLLomFPRU9u0XtIGKnF/cNByTuz9fceR6Pi6mQXZawv+OOMiBeu0gbyp
+F1uVe8PCshzCrWTE3UjRpQxy9gizvSbGZyGQi1Lm42JXKG3wAAAMEA4r4CLM1xsyxBBMld
+rxiTqy6bfrZjKkT5MPjBjp+57i5kW9NVqGCnIy/m98pLTuKjTCDmUuWQXS+oqhHw5vq/wj
+RvQYqkJDz1UGmC1lD2qyqERjOiWa8/iy4dXSLeHCT70+/xR2dBb0z8cT++yZEqLdEZSnHG
+yRaZMHot1OohVDqJS8nEbxOzgPGdopRMiX6ws/p5/k9YAGkHx0hszA8cn/Tk2/mdS5lugw
+Y7mdXzfcKvxkgoFrG7XowqRVrozcvDAAAAwQDU1ITasquNLaQhKNqiHx/N7bvKVO33icAx
+NdShqJEWx/g9idvQ25sA1Ubc1a+Ot5Lgfrs2OBKe+LgSmPAZOjv4ShqBHtsSh3am8/K1xR
+gQKgojLL4FhtgxtwoZrVvovZHGV3g2A28BRGbKIGVGPsOszJALU7jlLlcTHlB7SCQBI8FQ
+vTi2UEsfTmA22NnuVPITeqbmAQQXkSZcZbpbvdc0vQzp/3iOb/OCrIMET3HqVEMyQVsVs6
+xa9026AMTGLaEAAAATcm9vdEBvcGVuc3NoLXNlcnZlcg==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/e2e_tests/ssh-server-config/ssh_host_rsa_key.pub b/e2e_tests/ssh-server-config/ssh_host_rsa_key.pub
new file mode 100644
index 0000000..3288d9e
--- /dev/null
+++ b/e2e_tests/ssh-server-config/ssh_host_rsa_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8gZTSlGlOGEhxpI+uDb6/txeX5BwbSJxvjmF8PjjKsKheSkmnueEe/Q5tkvZjVwbt55rJgXgCoPeTBDtbz0Y3pg71v+aQmBrCTT9gtTKiNNL11txNt2R8/PJRq9MwOOXL48iN+JY4SdSA9/XLfE40uQlTQgvJOaBPNIgw10qeC10IZ/K0EK42I8pTyER0RW3zZ5uqC0uhJif1xRxVXPot7DNSHz5i2g9vxn8EhUTNTfseIT0w5ggFeOjUAQlKc4KSPCVWU/MuFxcpM+G3SLC/8qdk02DQsOadT/PSf6c+r8Qx+W6byNFhuCnQLmGSw91zx7R4Qp32BqS3sHS1rcyO3lvwc9B0EZkj9AW58SdI7F8magrgG+PL+hTATSEZTqmjmD7hPaPoGqqbYj7JURuQ/9SK4P+QSJPQLfIJXnjuAgOM3YwT20YkOR3TbcEW+qfey0VVVnlayKxEoZz6W9N+p4AnIoVpW3lrbfLrzO2MQxWswuSxG4QXyAbmRXsQbKM= root@openssh-server
diff --git a/e2e_tests/ssh-server-config/sshd_config b/e2e_tests/ssh-server-config/sshd_config
new file mode 100644
index 0000000..2112156
--- /dev/null
+++ b/e2e_tests/ssh-server-config/sshd_config
@@ -0,0 +1,117 @@
+#	$OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $
+
+# This is the sshd server system-wide configuration file.  See
+# sshd_config(5) for more information.
+
+# This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin
+
+# The strategy used for options in the default sshd_config shipped with
+# OpenSSH is to specify options with their default value where
+# possible, but leave them commented.  Uncommented options override the
+# default value.
+
+#Port 22
+#AddressFamily any
+#ListenAddress 0.0.0.0
+#ListenAddress ::
+
+#HostKey /etc/ssh/ssh_host_rsa_key
+#HostKey /etc/ssh/ssh_host_ecdsa_key
+#HostKey /etc/ssh/ssh_host_ed25519_key
+
+# Ciphers and keying
+#RekeyLimit default none
+
+# Logging
+#SyslogFacility AUTH
+#LogLevel INFO
+
+# Authentication:
+
+#LoginGraceTime 2m
+#PermitRootLogin prohibit-password
+#StrictModes yes
+#MaxAuthTries 6
+#MaxSessions 10
+
+#PubkeyAuthentication yes
+
+# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
+# but this is overridden so installations will only check .ssh/authorized_keys
+AuthorizedKeysFile	.ssh/authorized_keys
+
+#AuthorizedPrincipalsFile none
+
+#AuthorizedKeysCommand none
+#AuthorizedKeysCommandUser nobody
+
+# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
+#HostbasedAuthentication no
+# Change to yes if you don't trust ~/.ssh/known_hosts for
+# HostbasedAuthentication
+#IgnoreUserKnownHosts no
+# Don't read the user's ~/.rhosts and ~/.shosts files
+#IgnoreRhosts yes
+
+# To disable tunneled clear text passwords, change to no here!
+PasswordAuthentication no
+#PermitEmptyPasswords no
+
+# Change to no to disable s/key passwords
+#ChallengeResponseAuthentication yes
+
+# Kerberos options
+#KerberosAuthentication no
+#KerberosOrLocalPasswd yes
+#KerberosTicketCleanup yes
+#KerberosGetAFSToken no
+
+# GSSAPI options
+#GSSAPIAuthentication no
+#GSSAPICleanupCredentials yes
+
+# Set this to 'yes' to enable PAM authentication, account processing,
+# and session processing. If this is enabled, PAM authentication will
+# be allowed through the ChallengeResponseAuthentication and
+# PasswordAuthentication.  Depending on your PAM configuration,
+# PAM authentication via ChallengeResponseAuthentication may bypass
+# the setting of "PermitRootLogin without-password".
+# If you just want the PAM account and session checks to run without
+# PAM authentication, then enable this but set PasswordAuthentication
+# and ChallengeResponseAuthentication to 'no'.
+#UsePAM no
+
+#AllowAgentForwarding yes
+# Feel free to re-enable these if your use case requires them.
+AllowTcpForwarding yes
+GatewayPorts no
+X11Forwarding no
+#X11DisplayOffset 10
+#X11UseLocalhost yes
+#PermitTTY yes
+#PrintMotd yes
+#PrintLastLog yes
+#TCPKeepAlive yes
+#PermitUserEnvironment no
+#Compression delayed
+#ClientAliveInterval 0
+#ClientAliveCountMax 3
+#UseDNS no
+PidFile /config/sshd.pid
+#MaxStartups 10:30:100
+#PermitTunnel no
+#ChrootDirectory none
+#VersionAddendum none
+
+# no default banner path
+#Banner none
+
+# override default of no subsystems
+Subsystem	sftp	/usr/lib/ssh/sftp-server -u 022
+
+# Example of overriding settings on a per-user basis
+#Match User anoncvs
+#	X11Forwarding no
+#	AllowTcpForwarding no
+#	PermitTTY no
+#	ForceCommand cvs server
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b0471b7
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta:__legacy__"
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 1440405..ccaaab8 100644
--- a/setup.py
+++ b/setup.py
@@ -6,12 +6,9 @@ https://github.com/pypa/sampleproject
 """
 
 import re
-import sys
 from os import path
 from codecs import open  # To use a consistent encoding
-
 from setuptools import setup  # Always prefer setuptools over distutils
-from setuptools.command.test import test as TestCommand
 
 here = path.abspath(path.dirname(__file__))
 name = 'sshtunnel'
@@ -32,21 +29,6 @@ with open(path.join(here, name + '.py'), encoding='utf-8') as f:
     version = eval(re.search("__version__[ ]*=[ ]*([^\r\n]+)", data).group(1))
 
 
-class Tox(TestCommand):
-    """ Integration with tox """
-
-    def finalize_options(self):
-        TestCommand.finalize_options(self)
-        self.test_args = ['--recreate', '-v']
-        self.test_suite = True
-
-    def run_tests(self):
-        # import here, otherwise eggs aren't loaded
-        import tox
-        errcode = tox.cmdline(self.test_args)
-        sys.exit(errcode)
-
-
 setup(
     name=name,
 
@@ -57,14 +39,15 @@ setup(
 
     description=description,
     long_description='\n'.join((long_description, documentation, changelog)),
+    long_description_content_type='text/x-rst',
 
     # The project's main homepage.
     url=url,
     download_url=ppa + version + '.zip',  # noqa
 
     # Author details
-    author='Pahaz Blinov',
-    author_email='pahaz.blinov@gmail.com',
+    author='Pahaz White',
+    author_email='pahaz.white@gmail.com',
 
     # Choose your license
     license='MIT',
@@ -92,6 +75,8 @@ setup(
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
     ],
 
     platforms=['unix', 'macos', 'windows'],
@@ -112,13 +97,16 @@ setup(
     # requirements files see:
     # https://packaging.python.org/en/latest/requirements.html
     install_requires=[
-        'paramiko>=1.15.2',
+        'paramiko>=2.7.2',
     ],
 
     # List additional groups of dependencies here (e.g. development
     # dependencies). You can install these using the following syntax,
     # for example:
     # $ pip install -e .[dev,test]
+    tests_require=[
+        'tox>=1.8.1',
+    ],
     extras_require={
         'dev': ['check-manifest'],
         'test': [
@@ -146,6 +134,4 @@ setup(
         ]
     },
 
-    # Integrate tox with setuptools
-    cmdclass={'test': Tox},
 )
diff --git a/sshtunnel.egg-info/PKG-INFO b/sshtunnel.egg-info/PKG-INFO
index 6b2e89d..7f02d5c 100644
--- a/sshtunnel.egg-info/PKG-INFO
+++ b/sshtunnel.egg-info/PKG-INFO
@@ -1,22 +1,21 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
 Name: sshtunnel
-Version: 0.1.4
+Version: 0.4.0
 Summary: Pure python SSH tunnels
 Home-page: https://github.com/pahaz/sshtunnel
-Author: Pahaz Blinov
-Author-email: pahaz.blinov@gmail.com
+Author: Pahaz White
+Author-email: pahaz.white@gmail.com
 License: MIT
-Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip
-Description-Content-Type: UNKNOWN
+Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.4.0.zip
 Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         
         |pyversions| |license|
         
-        **Author**: `Pahaz Blinov`_
+        **Author**: `Pahaz`_
         
         **Repo**: https://github.com/pahaz/sshtunnel/
         
-        Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on
+        Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on
         Windows.
         
         See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py
@@ -112,12 +111,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind
         port.
         
-        .. code-block:: py
+        .. code-block:: python
         
             from sshtunnel import SSHTunnelForwarder
         
             server = SSHTunnelForwarder(
-                'pahaz.urfuclub.ru',
+                'alfa.8iq.dev',
                 ssh_username="pahaz",
                 ssh_password="secret",
                 remote_bind_address=('127.0.0.1', 8080)
@@ -137,12 +136,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         assuming password protected pkey authentication, remote server's SSH service is
         listening on port 443 and that port is open in the firewall (**Fig2**):
         
-        .. code-block:: py
+        .. code-block:: python
         
             import paramiko
-            from sshtunnel import SSHTunnelForwarder
+            import sshtunnel
         
-            with SSHTunnelForwarder(
+            with sshtunnel.open_tunnel(
                 (REMOTE_SERVER_IP, 443),
                 ssh_username="",
                 ssh_pkey="/var/ssh/rsa_key",
@@ -164,12 +163,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         
         Example of a port forwarding for the Vagrant MySQL local port:
         
-        .. code-block:: py
+        .. code-block:: python
         
-            from sshtunnel import SSHTunnelForwarder
+            from sshtunnel import open_tunnel
             from time import sleep
         
-            with SSHTunnelForwarder(
+            with open_tunnel(
                 ('localhost', 2222),
                 ssh_username="vagrant",
                 ssh_password="vagrant",
@@ -189,6 +188,40 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         
             (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost
         
+        Example 4
+        ---------
+        
+        Opening an SSH session jumping over two tunnels. SSH transport and tunnels
+        will be daemonised, which will not wait for the connections to stop at close
+        time.
+        
+        .. code-block:: python
+        
+            import sshtunnel
+            from paramiko import SSHClient
+        
+        
+            with sshtunnel.open_tunnel(
+                ssh_address_or_host=('GW1_ip', 20022),
+                remote_bind_address=('GW2_ip', 22),
+            ) as tunnel1:
+                print('Connection to tunnel1 (GW1_ip:GW1_port) OK...')
+                with sshtunnel.open_tunnel(
+                    ssh_address_or_host=('localhost', tunnel1.local_bind_port),
+                    remote_bind_address=('target_ip', 22),
+                    ssh_username='GW2_user',
+                    ssh_password='GW2_pwd',
+                ) as tunnel2:
+                    print('Connection to tunnel2 (GW2_ip:GW2_port) OK...')
+                    with SSHClient() as ssh:
+                        ssh.connect('localhost',
+                            port=tunnel2.local_bind_port,
+                            username='target_user',
+                            password='target_pwd',
+                        )
+                        ssh.exec_command(...)
+        
+        
         CLI usage
         =========
         
@@ -198,11 +231,12 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
             usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R
                              IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]]
                              [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v]
-                             [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]]
+                             [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n]
+                             [-d [FOLDER [FOLDER ...]]]
                              ssh_address
         
             Pure python ssh tunnel utils
-            Version 0.1.4
+            Version 0.4.0
         
             positional arguments:
               ssh_address           SSH server IP address (GW for SSH tunnels)
@@ -246,7 +280,7 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
               -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]]
                                     List of directories where SSH pkeys (in the format `id_*`) may be found
         
-        .. _Pahaz Blinov: https://github.com/pahaz
+        .. _Pahaz: https://github.com/pahaz
         .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel
         .. _paramiko: http://www.paramiko.org/
         .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg
@@ -285,10 +319,45 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         - `Dan Harbin`_
         - `Ignacio Peluffo`_
         - `Niels Zeilemaker`_
+        - `Georgy Rylov`_
+        - `Eddie Chiang`_
+        - `kkrasovskii`_
         
         CHANGELOG
         =========
         
+        - v.0.4.0 (`Pahaz`_)
+            + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_)
+            + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_)
+            + Add docker based end to end hangs tests (`#219`_)
+        
+        - v.0.3.2 (`Pahaz`_, `JM Fernández`_)
+            + Fix host key directory detection
+            + Unify default ssh config folder to `~/.ssh`
+        
+        - v.0.3.1 (`Pahaz`_)
+            + Increase open connection timeout to 10 secods
+        
+        - v.0.3.0 (`Pahaz`_)
+            + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible)
+            + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible)
+            + Set transport keepalive to 5 second by default (disabled for version < 0.3.0)
+            + Set default transport timeout to 0.1
+            + Deprecate and remove `block_on_close` option
+            + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_)
+        
+        - v.0.2.2 (`Pahaz`_)
+            + Add `.stop(force=True)` for force close active connections (`#201`_)
+        
+        - v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_)
+            + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_)
+        
+        - v.0.2.0 (`Georgy Rylov`_)
+            + Support IPv6 without proxy command. Use built-in paramiko create socket logic. The logic tries to use ipv6 socket family first, then ipv4 socket family.
+        
+        - v.0.1.5 (`JM Fernández`_)
+            + Introduce `block_on_close` attribute
+        
         - v.0.1.4 (`Niels Zeilemaker`_)
             + Allow loading pkeys from `~/.ssh`
         
@@ -305,7 +374,7 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         - v.0.1.0 (`JM Fernández`_)
             + Add `tunnel_bindings` property
             + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...)
-              (`Pahaz Blinov`_, `JM Fernández`_)
+              (`Pahaz`_, `JM Fernández`_)
             + Add TRACE logging level (`JM Fernández`_)
             + Code and tests refactoring (`JM Fernández`_)
             + Drop python3.2 support
@@ -331,55 +400,56 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
             + Add coverage (`JM Fernández`_)
             + Refactoring (`JM Fernández`_)
         
-        - v.0.0.6 (`Pahaz Blinov`_)
-            + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_)
+        - v.0.0.6 (`Pahaz`_)
+            + add ``-S`` CLI options for ssh private key password support (`Pahaz`_)
         
-        - v.0.0.5 (`Pahaz Blinov`_)
+        - v.0.0.5 (`Pahaz`_)
             + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_)
             + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_)
             + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_)
             + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_)
-            + add tests (`Pahaz Blinov`_)
-            + add CI integration (`Pahaz Blinov`_)
-            + normal packaging (`Pahaz Blinov`_)
-            + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior]
-            + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior]
+            + add tests (`Pahaz`_)
+            + add CI integration (`Pahaz`_)
+            + normal packaging (`Pahaz`_)
+            + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior]
+            + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior]
         
-        - v.0.0.4.4 (`Pahaz Blinov`_)
-           + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_)
+        - v.0.0.4.4 (`Pahaz`_)
+           + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_)
         
-        - v.0.0.4.3 (`Pahaz Blinov`_)
-            + fix default port issue `#19`_ (`Pahaz Blinov`_)
+        - v.0.0.4.3 (`Pahaz`_)
+            + fix default port issue `#19`_ (`Pahaz`_)
         
-        - v.0.0.4.2 (`Pahaz Blinov`_)
+        - v.0.0.4.2 (`Pahaz`_)
             + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_)
         
-        - v.0.0.4.1 (`Pahaz Blinov`_)
-            + fix CLI issues `#13`_ (`Pahaz Blinov`_)
+        - v.0.0.4.1 (`Pahaz`_)
+            + fix CLI issues `#13`_ (`Pahaz`_)
         
-        - v.0.0.4 (`Pahaz Blinov`_)
-            + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible*
-            + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
-            + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
+        - v.0.0.4 (`Pahaz`_)
+            + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible*
+            + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
+            + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
             + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible*
             + add CLI interface (`JM Fernández`_)
             + support opening several tunnels at once (`JM Fernández`_)
-            + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_)
-            + improve logging (`JM Fernández`_, `Pahaz Blinov`_)
-            + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_)
+            + improve stability and readability (`JM Fernández`_, `Pahaz`_)
+            + improve logging (`JM Fernández`_, `Pahaz`_)
+            + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_)
             + add ``ssh_config_file`` argument support (`JM Fernández`_)
-            + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_)
+            + add Python 3 support (`JM Fernández`_, `Pahaz`_)
         
-        - v.0.0.3 (`Pahaz Blinov`_)
+        - v.0.0.3 (`Pahaz`_)
             + add ``threaded`` option (`Cameron Maske`_)
             + fix exception error message, correctly printing destination address (`Gustavo Machado`_)
-            + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_)
+            + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_)
         
-        - v.0.0.1 (`Pahaz Blinov`_)
-            + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_)
-            + ``open`` function (`Pahaz Blinov`_)
+        - v.0.0.1 (`Pahaz`_)
+            + ``SSHTunnelForwarder`` class (`Pahaz`_)
+            + ``open`` function (`Pahaz`_)
         
         
+        .. _Pahaz: https://github.com/pahaz
         .. _Cameron Maske: https://github.com/cameronmaske
         .. _Gustavo Machado: https://github.com/gdmachado
         .. _Colin Jermain: https://github.com/cjermain
@@ -391,6 +461,9 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         .. _Dan Harbin: https://github.com/RasterBurn
         .. _Ignacio Peluffo: https://github.com/ipeluffo
         .. _Niels Zeilemaker: https://github.com/NielsZeilemaker
+        .. _Georgy Rylov: https://github.com/g0djan
+        .. _Eddie Chiang: https://github.com/eddie-chiang
+        .. _kkrasovskii: https://github.com/kkrasovskii
         .. _#13: https://github.com/pahaz/sshtunnel/issues/13
         .. _#16: https://github.com/pahaz/sshtunnel/issues/16
         .. _#19: https://github.com/pahaz/sshtunnel/issues/19
@@ -404,6 +477,13 @@ Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
         .. _#41: https://github.com/pahaz/sshtunnel/issues/41
         .. _#43: https://github.com/pahaz/sshtunnel/issues/43
         .. _#46: https://github.com/pahaz/sshtunnel/issues/46
+        .. _#170: https://github.com/pahaz/sshtunnel/issues/170
+        .. _#201: https://github.com/pahaz/sshtunnel/issues/201
+        .. _#162: https://github.com/pahaz/sshtunnel/issues/162
+        .. _#173: https://github.com/pahaz/sshtunnel/issues/173
+        .. _#201: https://github.com/pahaz/sshtunnel/issues/201
+        .. _#211: https://github.com/pahaz/sshtunnel/issues/211
+        .. _#219: https://github.com/pahaz/sshtunnel/issues/219
         .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127
         
 Keywords: ssh tunnel paramiko proxy tcp-forward
@@ -420,3 +500,9 @@ Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Description-Content-Type: text/x-rst
+Provides-Extra: dev
+Provides-Extra: test
+Provides-Extra: build_sphinx
diff --git a/sshtunnel.egg-info/SOURCES.txt b/sshtunnel.egg-info/SOURCES.txt
index d96700f..267c5d4 100644
--- a/sshtunnel.egg-info/SOURCES.txt
+++ b/sshtunnel.egg-info/SOURCES.txt
@@ -4,13 +4,26 @@ README.rst
 Troubleshoot.rst
 changelog.rst
 docs.rst
+pyproject.toml
 setup.cfg
 setup.py
 sshtunnel.py
 docs/Makefile
 docs/conf.py
 docs/index.rst
-docs/requirements-docs.txt
+docs/requirements.txt
+e2e_tests/docker-compose.yaml
+e2e_tests/run_docker_e2e_db_tests.py
+e2e_tests/run_docker_e2e_hangs_tests.py
+e2e_tests/ssh-server-config/ssh_host_dsa_key
+e2e_tests/ssh-server-config/ssh_host_dsa_key.pub
+e2e_tests/ssh-server-config/ssh_host_ecdsa_key
+e2e_tests/ssh-server-config/ssh_host_ecdsa_key.pub
+e2e_tests/ssh-server-config/ssh_host_ed25519_key
+e2e_tests/ssh-server-config/ssh_host_ed25519_key.pub
+e2e_tests/ssh-server-config/ssh_host_rsa_key
+e2e_tests/ssh-server-config/ssh_host_rsa_key.pub
+e2e_tests/ssh-server-config/sshd_config
 sshtunnel.egg-info/PKG-INFO
 sshtunnel.egg-info/SOURCES.txt
 sshtunnel.egg-info/dependency_links.txt
@@ -18,7 +31,8 @@ sshtunnel.egg-info/entry_points.txt
 sshtunnel.egg-info/requires.txt
 sshtunnel.egg-info/top_level.txt
 tests/__init__.py
-tests/__init__.pyc
+tests/requirements-syntax.txt
+tests/requirements.txt
 tests/test_forwarder.py
 tests/testconfig
 tests/testrsa.key
diff --git a/sshtunnel.egg-info/requires.txt b/sshtunnel.egg-info/requires.txt
index 948948c..5146a0d 100644
--- a/sshtunnel.egg-info/requires.txt
+++ b/sshtunnel.egg-info/requires.txt
@@ -1,4 +1,4 @@
-paramiko>=1.15.2
+paramiko>=2.7.2
 
 [build_sphinx]
 sphinx
diff --git a/sshtunnel.py b/sshtunnel.py
index 9774698..52a23b3 100644
--- a/sshtunnel.py
+++ b/sshtunnel.py
@@ -29,42 +29,43 @@ if sys.version_info[0] < 3:  # pragma: no cover
     import SocketServer as socketserver
     string_types = basestring,  # noqa
     input_ = raw_input  # noqa
-else:
+else:  # pragma: no cover
     import queue
     import socketserver
     string_types = str
     input_ = input
 
 
-__version__ = '0.1.4'
+__version__ = '0.4.0'
 __author__ = 'pahaz'
 
 
-DEFAULT_LOGLEVEL = logging.ERROR  #: default level if no logger passed (ERROR)
-TUNNEL_TIMEOUT = 1.0  #: Timeout (seconds) for tunnel connection
-DAEMON = False
-TRACE_LEVEL = 1
+#: Timeout (seconds) for transport socket (``socket.settimeout``)
+SSH_TIMEOUT = 0.1  # ``None`` may cause a block of transport thread
+#: Timeout (seconds) for tunnel connection (open_channel timeout)
+TUNNEL_TIMEOUT = 10.0
+
+_DAEMON = True  #: Use daemon threads in connections
 _CONNECTION_COUNTER = 1
 _LOCK = threading.Lock()
-#: Timeout (seconds) for the connection to the SSH gateway, ``None`` to disable
-SSH_TIMEOUT = None
-DEPRECATIONS = {
+_DEPRECATIONS = {
     'ssh_address': 'ssh_address_or_host',
     'ssh_host': 'ssh_address_or_host',
     'ssh_private_key': 'ssh_pkey',
     'raise_exception_if_any_forwarder_have_a_problem': 'mute_exceptions'
 }
 
+# logging
+DEFAULT_LOGLEVEL = logging.ERROR  #: default level if no logger passed (ERROR)
+TRACE_LEVEL = 1
 logging.addLevelName(TRACE_LEVEL, 'TRACE')
+DEFAULT_SSH_DIRECTORY = '~/.ssh'
 
-if os.name == 'posix':
-    DEFAULT_SSH_DIRECTORY = '~/.ssh'
-    UnixStreamServer = socketserver.UnixStreamServer
-else:
-    DEFAULT_SSH_DIRECTORY = '~/ssh'
-    UnixStreamServer = socketserver.TCPServer
+_StreamServer = socketserver.UnixStreamServer if os.name == 'posix' \
+    else socketserver.TCPServer
 
 #: Path of optional ssh configuration file
+DEFAULT_SSH_DIRECTORY = '~/.ssh'
 SSH_CONFIG_FILE = os.path.join(DEFAULT_SSH_DIRECTORY, 'config')
 
 ########################
@@ -190,7 +191,7 @@ def create_logger(logger=None,
         :class:`logging.Logger`
     """
     logger = logger or logging.getLogger(
-        '{0}.SSHTunnelForwarder'.format(__name__)
+        'sshtunnel.SSHTunnelForwarder'
     )
     if not any(isinstance(x, logging.Handler) for x in logger.handlers):
         logger.setLevel(loglevel or DEFAULT_LOGLEVEL)
@@ -307,23 +308,33 @@ class _ForwardHandler(socketserver.BaseRequestHandler):
             if self.request in rqst:
                 data = self.request.recv(1024)
                 if not data:
+                    self.logger.log(
+                        TRACE_LEVEL,
+                        '>>> OUT {0} recv empty data >>>'.format(self.info)
+                    )
                     break
-                self.logger.log(TRACE_LEVEL,
-                                '>>> OUT {0} send to {1}: {2} >>>'.format(
-                                    self.info,
-                                    self.remote_address,
-                                    hexlify(data)
-                                ))
-                chan.send(data)
+                self.logger.log(
+                    TRACE_LEVEL,
+                    '>>> OUT {0} send to {1}: {2} >>>'.format(
+                        self.info,
+                        self.remote_address,
+                        hexlify(data)
+                    )
+                )
+                chan.sendall(data)
             if chan in rqst:  # else
                 if not chan.recv_ready():
+                    self.logger.log(
+                        TRACE_LEVEL,
+                        '<<< IN {0} recv is not ready <<<'.format(self.info)
+                    )
                     break
                 data = chan.recv(1024)
                 self.logger.log(
                     TRACE_LEVEL,
                     '<<< IN {0} recv: {1} <<<'.format(self.info, hexlify(data))
                 )
-                self.request.send(data)
+                self.request.sendall(data)
 
     def handle(self):
         uid = get_connection_id()
@@ -339,15 +350,12 @@ class _ForwardHandler(socketserver.BaseRequestHandler):
                 src_addr=src_address,
                 timeout=TUNNEL_TIMEOUT
             )
-        except paramiko.SSHException:
-            chan = None
-        if chan is None:
-            msg = '{0} to {1} was rejected by the SSH server'.format(
-                self.info,
-                self.remote_address
-            )
-            self.logger.log(TRACE_LEVEL, msg)
-            raise HandlerSSHTunnelForwarderError(msg)
+        except Exception as e:  # pragma: no cover
+            msg_tupe = 'ssh ' if isinstance(e, paramiko.SSHException) else ''
+            exc_msg = 'open new channel {0}error: {1}'.format(msg_tupe, e)
+            log_msg = '{0} {1}'.format(self.info, exc_msg)
+            self.logger.log(TRACE_LEVEL, log_msg)
+            raise HandlerSSHTunnelForwarderError(exc_msg)
 
         self.logger.log(TRACE_LEVEL, '{0} connected'.format(self.info))
         try:
@@ -376,14 +384,23 @@ class _ForwardServer(socketserver.TCPServer):  # Not Threading
 
     def __init__(self, *args, **kwargs):
         self.logger = create_logger(kwargs.pop('logger', None))
-        self.tunnel_ok = queue.Queue()
+        self.tunnel_ok = queue.Queue(1)
         socketserver.TCPServer.__init__(self, *args, **kwargs)
 
     def handle_error(self, request, client_address):
         (exc_class, exc, tb) = sys.exc_info()
-        self.logger.error('Could not establish connection from {0} to remote '
-                          'side of the tunnel'.format(request.getsockname()))
-        self.tunnel_ok.put(False)
+        local_side = request.getsockname()
+        remote_side = self.remote_address
+        self.logger.error('Could not establish connection from local {0} '
+                          'to remote {1} side of the tunnel: {2}'
+                          .format(local_side, remote_side, exc))
+        try:
+            self.tunnel_ok.put(False, block=False, timeout=0.1)
+        except queue.Full:
+            # wait untill tunnel_ok.get is called
+            pass
+        except exc:
+            self.logger.error('unexpected internal error: {0}'.format(exc))
 
     @property
     def local_address(self):
@@ -415,18 +432,19 @@ class _ThreadingForwardServer(socketserver.ThreadingMixIn, _ForwardServer):
     Allow concurrent connections to each tunnel
     """
     # If True, cleanly stop threads created by ThreadingMixIn when quitting
-    daemon_threads = DAEMON
+    # This value is overrides by SSHTunnelForwarder.daemon_forward_servers
+    daemon_threads = _DAEMON
 
 
-class _UnixStreamForwardServer(UnixStreamServer):
+class _StreamForwardServer(_StreamServer):
     """
-    Serve over UNIX domain sockets (does not work on Windows)
+    Serve over domain sockets (does not work on Windows)
     """
 
     def __init__(self, *args, **kwargs):
         self.logger = create_logger(kwargs.pop('logger', None))
-        self.tunnel_ok = queue.Queue()
-        UnixStreamServer.__init__(self, *args, **kwargs)
+        self.tunnel_ok = queue.Queue(1)
+        _StreamServer.__init__(self, *args, **kwargs)
 
     @property
     def local_address(self):
@@ -453,13 +471,14 @@ class _UnixStreamForwardServer(UnixStreamServer):
         return self.RequestHandlerClass.remote_address[1]
 
 
-class _ThreadingUnixStreamForwardServer(socketserver.ThreadingMixIn,
-                                        _UnixStreamForwardServer):
+class _ThreadingStreamForwardServer(socketserver.ThreadingMixIn,
+                                    _StreamForwardServer):
     """
     Allow concurrent connections to each tunnel
     """
     # If True, cleanly stop threads created by ThreadingMixIn when quitting
-    daemon_threads = DAEMON
+    # This value is overrides by SSHTunnelForwarder.daemon_forward_servers
+    daemon_threads = _DAEMON
 
 
 class SSHTunnelForwarder(object):
@@ -614,9 +633,8 @@ class SSHTunnelForwarder(object):
 
         host_pkey_directories (list):
             Look for pkeys in folders on this list, for example ['~/.ssh'].
-            An empty list disables this feature
 
-            Default: ``None``
+            Default: ``None`` (disabled)
 
             .. versionadded:: 0.1.4
 
@@ -649,9 +667,10 @@ class SSHTunnelForwarder(object):
             Interval in seconds defining the period in which, if no data
             was sent over the connection, a *'keepalive'* packet will be
             sent (and ignored by the remote host). This can be useful to
-            keep connections alive over a NAT
+            keep connections alive over a NAT. You can set to 0.0 for
+            disable keepalive.
 
-            Default: 0.0 (no keepalive packets are sent)
+            Default: 5.0 (no keepalive packets are sent)
 
             .. versionadded:: 0.0.7
 
@@ -720,8 +739,10 @@ class SSHTunnelForwarder(object):
 
     """
     skip_tunnel_checkup = True
-    daemon_forward_servers = DAEMON  #: flag tunnel threads in daemon mode
-    daemon_transport = DAEMON  #: flag SSH transport thread in daemon mode
+    # This option affects the `ForwardServer` and all his threads
+    daemon_forward_servers = _DAEMON  #: flag tunnel threads in daemon mode
+    # This option affect only `Transport` thread
+    daemon_transport = _DAEMON  #: flag SSH transport thread in daemon mode
 
     def local_is_up(self, target):
         """
@@ -747,12 +768,59 @@ class SSHTunnelForwarder(object):
                                 'target can be a valid UNIX domain socket.')
             return False
 
-        if self.skip_tunnel_checkup:  # force tunnel check at this point
-            self.skip_tunnel_checkup = False
-            self.check_tunnels()
-            self.skip_tunnel_checkup = True  # roll it back
+        self.check_tunnels()
         return self.tunnel_is_up.get(target, True)
 
+    def check_tunnels(self):
+        """
+        Check that if all tunnels are established and populates
+        :attr:`.tunnel_is_up`
+        """
+        skip_tunnel_checkup = self.skip_tunnel_checkup
+        try:
+            # force tunnel check at this point
+            self.skip_tunnel_checkup = False
+            for _srv in self._server_list:
+                self._check_tunnel(_srv)
+        finally:
+            self.skip_tunnel_checkup = skip_tunnel_checkup  # roll it back
+
+    def _check_tunnel(self, _srv):
+        """ Check if tunnel is already established """
+        if self.skip_tunnel_checkup:
+            self.tunnel_is_up[_srv.local_address] = True
+            return
+        self.logger.info('Checking tunnel to: {0}'.format(_srv.remote_address))
+        if isinstance(_srv.local_address, string_types):  # UNIX stream
+            s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        else:
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s.settimeout(TUNNEL_TIMEOUT)
+        try:
+            # Windows raises WinError 10049 if trying to connect to 0.0.0.0
+            connect_to = ('127.0.0.1', _srv.local_port) \
+                if _srv.local_host == '0.0.0.0' else _srv.local_address
+            s.connect(connect_to)
+            self.tunnel_is_up[_srv.local_address] = _srv.tunnel_ok.get(
+                timeout=TUNNEL_TIMEOUT * 1.1
+            )
+            self.logger.debug(
+                'Tunnel to {0} is DOWN'.format(_srv.remote_address)
+            )
+        except socket.error:
+            self.logger.debug(
+                'Tunnel to {0} is DOWN'.format(_srv.remote_address)
+            )
+            self.tunnel_is_up[_srv.local_address] = False
+
+        except queue.Empty:
+            self.logger.debug(
+                'Tunnel to {0} is UP'.format(_srv.remote_address)
+            )
+            self.tunnel_is_up[_srv.local_address] = True
+        finally:
+            s.close()
+
     def _make_ssh_forward_handler_class(self, remote_address_):
         """
         Make SSH Handler class
@@ -766,9 +834,9 @@ class SSHTunnelForwarder(object):
     def _make_ssh_forward_server_class(self, remote_address_):
         return _ThreadingForwardServer if self._threaded else _ForwardServer
 
-    def _make_unix_ssh_forward_server_class(self, remote_address_):
-        return _ThreadingUnixStreamForwardServer if \
-            self._threaded else _UnixStreamForwardServer
+    def _make_stream_ssh_forward_server_class(self, remote_address_):
+        return _ThreadingStreamForwardServer if self._threaded \
+            else _StreamForwardServer
 
     def _make_ssh_forward_server(self, remote_address, local_bind_address):
         """
@@ -776,10 +844,9 @@ class SSHTunnelForwarder(object):
         """
         _Handler = self._make_ssh_forward_handler_class(remote_address)
         try:
-            if isinstance(local_bind_address, string_types):
-                forward_maker_class = self._make_unix_ssh_forward_server_class
-            else:
-                forward_maker_class = self._make_ssh_forward_server_class
+            forward_maker_class = self._make_stream_ssh_forward_server_class \
+                if isinstance(local_bind_address, string_types) \
+                else self._make_ssh_forward_server_class
             _Server = forward_maker_class(remote_address)
             ssh_forward_server = _Server(
                 local_bind_address,
@@ -826,7 +893,7 @@ class SSHTunnelForwarder(object):
             mute_exceptions=False,
             remote_bind_address=None,
             remote_bind_addresses=None,
-            set_keepalive=0.0,
+            set_keepalive=5.0,
             threaded=True,  # old version False
             compression=None,
             allow_agent=True,  # look for keys from an SSH agent
@@ -1019,16 +1086,21 @@ class SSHTunnelForwarder(object):
         keys = SSHTunnelForwarder.get_agent_keys(logger=logger) \
             if allow_agent else []
 
-        if host_pkey_directories is not None:
-            paramiko_key_types = {'rsa': paramiko.RSAKey,
-                                  'dsa': paramiko.DSSKey,
-                                  'ecdsa': paramiko.ECDSAKey,
-                                  'ed25519': paramiko.Ed25519Key}
-            for directory in host_pkey_directories or [DEFAULT_SSH_DIRECTORY]:
-                for keytype in paramiko_key_types.keys():
-                    ssh_pkey_expanded = os.path.expanduser(
-                        os.path.join(directory, 'id_{}'.format(keytype))
-                    )
+        if host_pkey_directories is None:
+            host_pkey_directories = [DEFAULT_SSH_DIRECTORY]
+
+        paramiko_key_types = {'rsa': paramiko.RSAKey,
+                              'dsa': paramiko.DSSKey,
+                              'ecdsa': paramiko.ECDSAKey}
+        if hasattr(paramiko, 'Ed25519Key'):
+            # NOQA: new in paramiko>=2.2: http://docs.paramiko.org/en/stable/api/keys.html#module-paramiko.ed25519key
+            paramiko_key_types['ed25519'] = paramiko.Ed25519Key
+        for directory in host_pkey_directories:
+            for keytype in paramiko_key_types.keys():
+                ssh_pkey_expanded = os.path.expanduser(
+                    os.path.join(directory, 'id_{}'.format(keytype))
+                )
+                try:
                     if os.path.isfile(ssh_pkey_expanded):
                         ssh_pkey = SSHTunnelForwarder.read_private_key_file(
                             pkey_file=ssh_pkey_expanded,
@@ -1037,11 +1109,12 @@ class SSHTunnelForwarder(object):
                         )
                         if ssh_pkey:
                             keys.append(ssh_pkey)
+                except OSError as exc:
+                    if logger:
+                        logger.warning('Private key file {0} check error: {1}'
+                                       .format(ssh_pkey_expanded, exc))
         if logger:
-            logger.info('{0} keys loaded from host directory'.format(
-                len(keys))
-            )
-
+            logger.info('{0} key(s) loaded'.format(len(keys)))
         return keys
 
     @staticmethod
@@ -1090,7 +1163,7 @@ class SSHTunnelForwarder(object):
                 logger.warning('Private key file not found: {0}'
                                .format(ssh_pkey))
         if isinstance(ssh_pkey, paramiko.pkey.PKey):
-            ssh_loaded_pkeys.append(ssh_pkey)
+            ssh_loaded_pkeys.insert(0, ssh_pkey)
 
         if not ssh_password and not ssh_loaded_pkeys:
             raise ValueError('No password or public key available!')
@@ -1112,15 +1185,24 @@ class SSHTunnelForwarder(object):
             self.logger.debug('Connecting via proxy: {0}'.format(proxy_repr))
             _socket = self.ssh_proxy
         else:
-            _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            _socket = (self.ssh_host, self.ssh_port)
         if isinstance(_socket, socket.socket):
             _socket.settimeout(SSH_TIMEOUT)
             _socket.connect((self.ssh_host, self.ssh_port))
         transport = paramiko.Transport(_socket)
+        sock = transport.sock
+        if isinstance(sock, socket.socket):
+            sock.settimeout(SSH_TIMEOUT)
         transport.set_keepalive(self.set_keepalive)
         transport.use_compression(compress=self.compression)
         transport.daemon = self.daemon_transport
-
+        # try to solve https://github.com/paramiko/paramiko/issues/1181
+        # transport.banner_timeout = 200
+        if isinstance(sock, socket.socket):
+            sock_timeout = sock.gettimeout()
+            sock_info = repr((sock.family, sock.type, sock.proto))
+            self.logger.debug('Transport socket info: {0}, timeout={1}'
+                              .format(sock_info, sock_timeout))
         return transport
 
     def _create_tunnels(self):
@@ -1177,19 +1259,19 @@ class SSHTunnelForwarder(object):
         """
         Processes optional deprecate arguments
         """
-        if deprecated_attrib not in DEPRECATIONS:
+        if deprecated_attrib not in _DEPRECATIONS:
             raise ValueError('{0} not included in deprecations list'
                              .format(deprecated_attrib))
         if deprecated_attrib in kwargs:
             warnings.warn("'{0}' is DEPRECATED use '{1}' instead"
                           .format(deprecated_attrib,
-                                  DEPRECATIONS[deprecated_attrib]),
+                                  _DEPRECATIONS[deprecated_attrib]),
                           DeprecationWarning)
             if attrib:
                 raise ValueError("You can't use both '{0}' and '{1}'. "
                                  "Please only use one of them"
                                  .format(deprecated_attrib,
-                                         DEPRECATIONS[deprecated_attrib]))
+                                         _DEPRECATIONS[deprecated_attrib]))
             else:
                 return kwargs.pop(deprecated_attrib)
         return attrib
@@ -1213,12 +1295,11 @@ class SSHTunnelForwarder(object):
             paramiko.Pkey
         """
         ssh_pkey = None
-        for pkey_class in (key_type,) if key_type else (
-            paramiko.RSAKey,
-            paramiko.DSSKey,
-            paramiko.ECDSAKey,
-            paramiko.Ed25519Key
-        ):
+        key_types = (paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey)
+        if hasattr(paramiko, 'Ed25519Key'):
+            # NOQA: new in paramiko>=2.2: http://docs.paramiko.org/en/stable/api/keys.html#module-paramiko.ed25519key
+            key_types += (paramiko.Ed25519Key, )
+        for pkey_class in (key_type,) if key_type else key_types:
             try:
                 ssh_pkey = pkey_class.from_private_key_file(
                     pkey_file,
@@ -1240,50 +1321,6 @@ class SSHTunnelForwarder(object):
                                  .format(pkey_file, pkey_class))
         return ssh_pkey
 
-    def _check_tunnel(self, _srv):
-        """ Check if tunnel is already established """
-        if self.skip_tunnel_checkup:
-            self.tunnel_is_up[_srv.local_address] = True
-            return
-        self.logger.info('Checking tunnel to: {0}'.format(_srv.remote_address))
-        if isinstance(_srv.local_address, string_types):  # UNIX stream
-            s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        else:
-            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        s.settimeout(TUNNEL_TIMEOUT)
-        try:
-            # Windows raises WinError 10049 if trying to connect to 0.0.0.0
-            connect_to = ('127.0.0.1', _srv.local_port) \
-                if _srv.local_host == '0.0.0.0' else _srv.local_address
-            s.connect(connect_to)
-            self.tunnel_is_up[_srv.local_address] = _srv.tunnel_ok.get(
-                timeout=TUNNEL_TIMEOUT * 1.1
-            )
-            self.logger.debug(
-                'Tunnel to {0} is DOWN'.format(_srv.remote_address)
-            )
-        except socket.error:
-            self.logger.debug(
-                'Tunnel to {0} is DOWN'.format(_srv.remote_address)
-            )
-            self.tunnel_is_up[_srv.local_address] = False
-
-        except queue.Empty:
-            self.logger.debug(
-                'Tunnel to {0} is UP'.format(_srv.remote_address)
-            )
-            self.tunnel_is_up[_srv.local_address] = True
-        finally:
-            s.close()
-
-    def check_tunnels(self):
-        """
-        Check that if all tunnels are established and populates
-        :attr:`.tunnel_is_up`
-        """
-        for _srv in self._server_list:
-            self._check_tunnel(_srv)
-
     def start(self):
         """ Start the SSH tunnels """
         if self.is_alive:
@@ -1307,9 +1344,18 @@ class SSHTunnelForwarder(object):
             self._raise(HandlerSSHTunnelForwarderError,
                         'An error occurred while opening tunnels.')
 
-    def stop(self):
+    def stop(self, force=False):
         """
-        Shut the tunnel down.
+        Shut the tunnel down. By default we are always waiting until closing
+        all connections. You can use `force=True` to force close connections
+
+        Keyword Arguments:
+            force (bool):
+                Force close current connections
+
+                Default: False
+
+                .. versionadded:: 0.2.2
 
         .. note:: This **had** to be handled with care before ``0.1.0``:
 
@@ -1330,7 +1376,7 @@ class SSHTunnelForwarder(object):
             (address_to_str(k.local_address) for k in self._server_list)
         ) or 'None'
         self.logger.debug('Listening tunnels: ' + opened_address_text)
-        self._stop_transport()
+        self._stop_transport(force=force)
         self._server_list = []  # reset server list
         self.tunnel_is_up = {}  # reset tunnel status
 
@@ -1395,28 +1441,37 @@ class SSHTunnelForwarder(object):
             address_to_str(_srv.remote_address))
         )
 
-    def _stop_transport(self):
+    def _stop_transport(self, force=False):
         """ Close the underlying transport when nothing more is needed """
         try:
             self._check_is_started()
         except (BaseSSHTunnelForwarderError,
                 HandlerSSHTunnelForwarderError) as e:
             self.logger.warning(e)
+        if force and self.is_active:
+            # don't wait connections
+            self.logger.info('Closing ssh transport')
+            self._transport.close()
+            self._transport.stop_thread()
         for _srv in self._server_list:
-            tunnel = _srv.local_address
-            if self.tunnel_is_up[tunnel]:
-                self.logger.info('Shutting down tunnel {0}'.format(tunnel))
-                _srv.shutdown()
+            status = 'up' if self.tunnel_is_up[_srv.local_address] else 'down'
+            self.logger.info('Shutting down tunnel: {0} <> {1} ({2})'.format(
+                address_to_str(_srv.local_address),
+                address_to_str(_srv.remote_address),
+                status
+            ))
+            _srv.shutdown()
             _srv.server_close()
             # clean up the UNIX domain socket if we're using one
-            if isinstance(_srv, _UnixStreamForwardServer):
+            if isinstance(_srv, _StreamForwardServer):
                 try:
                     os.unlink(_srv.local_address)
                 except Exception as e:
                     self.logger.error('Unable to unlink socket {0}: {1}'
-                                      .format(self.local_address, repr(e)))
+                                      .format(_srv.local_address, repr(e)))
         self.is_alive = False
         if self.is_active:
+            self.logger.info('Closing ssh transport')
             self._transport.close()
             self._transport.stop_thread()
         self.logger.debug('Transport is closed')
@@ -1533,7 +1588,7 @@ class SSHTunnelForwarder(object):
             self.ssh_proxy.cmd[1] if self.ssh_proxy else 'no',
             self.ssh_username,
             credentials,
-            self.ssh_host_key if self.ssh_host_key else'not checked',
+            self.ssh_host_key if self.ssh_host_key else 'not checked',
             '' if self.is_alive else 'not ',
             'disabled' if not self.set_keepalive else
             'every {0} sec'.format(self.set_keepalive),
@@ -1556,7 +1611,15 @@ class SSHTunnelForwarder(object):
             self.__exit__()
 
     def __exit__(self, *args):
-        self._stop_transport()
+        self.stop(force=True)
+
+    def __del__(self):
+        if self.is_active or self.is_alive:
+            self.logger.warning(
+                "It looks like you didn't call the .stop() before "
+                "the SSHTunnelForwarder obj was collected by "
+                "the garbage collector! Running .stop(force=True)")
+            self.stop(force=True)
 
 
 def open_tunnel(*args, **kwargs):
@@ -1615,8 +1678,15 @@ def open_tunnel(*args, **kwargs):
             kwargs
         )
 
-    ssh_port = kwargs.pop('ssh_port', None)
+    ssh_port = kwargs.pop('ssh_port', 22)
     skip_tunnel_checkup = kwargs.pop('skip_tunnel_checkup', True)
+    block_on_close = kwargs.pop('block_on_close', None)
+    if block_on_close:
+        warnings.warn("'block_on_close' is DEPRECATED. You should use either"
+                      " .stop() or .stop(force=True), depends on what you do"
+                      " with the active connections. This option has no"
+                      " affect since 0.3.0",
+                      DeprecationWarning)
     if not args:
         if isinstance(ssh_address_or_host, tuple):
             args = (ssh_address_or_host, )
@@ -1808,7 +1878,7 @@ def _parse_arguments(args=None):
     return vars(parser.parse_args(args))
 
 
-def _cli_main(args=None):
+def _cli_main(args=None, **extras):
     """ Pass input arguments to open_tunnel
 
         Mandatory: ssh_address, -R (remote bind address list)
@@ -1840,6 +1910,9 @@ def _cli_main(args=None):
               logging.DEBUG,
               TRACE_LEVEL]
     arguments.setdefault('debug_level', levels[verbosity])
+    # do this while supporting py27/py34 instead of merging dicts
+    for (extra, value) in extras.items():
+        arguments.setdefault(extra, value)
     with open_tunnel(**arguments) as tunnel:
         if tunnel.is_alive:
             input_('''
diff --git a/tests/__init__.pyc b/tests/__init__.pyc
deleted file mode 100644
index 4d58f5e..0000000
Binary files a/tests/__init__.pyc and /dev/null differ
diff --git a/tests/requirements-syntax.txt b/tests/requirements-syntax.txt
new file mode 100644
index 0000000..8e66e79
--- /dev/null
+++ b/tests/requirements-syntax.txt
@@ -0,0 +1,8 @@
+bashtest
+check-manifest
+docutils
+flake8
+mccabe
+pygments
+readme
+twine
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000..f9eba80
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,11 @@
+coveralls
+mock
+pytest
+pytest-cov
+pytest-xdist
+twine
+# readme-renderer (required by twine) 25.0 has removed support for Python 3.4
+readme-renderer>21.0,<25.0; python_version == '3.4'
+# try to solve CI problem
+importlib-metadata==1.7.0; python_version == '3.5'
+importlib-metadata==1.1.3; python_version == '3.4'
diff --git a/tests/test_forwarder.py b/tests/test_forwarder.py
index 30cf285..40662d0 100644
--- a/tests/test_forwarder.py
+++ b/tests/test_forwarder.py
@@ -74,6 +74,7 @@ open_tunnel = partial(
     ssh_config_file=None,
     allow_agent=False,
     skip_tunnel_checkup=True,
+    host_pkey_directories=[],
 )
 
 # CONSTANTS
@@ -169,23 +170,19 @@ class NullServer(paramiko.ServerInterface):
         return paramiko.AUTH_SUCCESSFUL if _ok else paramiko.AUTH_FAILED
 
     def check_channel_request(self, kind, chanid):
-        self.log.debug('NullServer.check_channel_request()'
-                       .format(kind, chanid))
+        self.log.debug('NullServer.check_channel_request()')
         return paramiko.OPEN_SUCCEEDED
 
     def check_channel_exec_request(self, channel, command):
-        self.log.debug('NullServer.check_channel_exec_request()'
-                       .format(channel, command))
+        self.log.debug('NullServer.check_channel_exec_request()')
         return True
 
     def check_port_forward_request(self, address, port):
-        self.log.debug('NullServer.check_port_forward_request()'
-                       .format(address, port))
+        self.log.debug('NullServer.check_port_forward_request()')
         return True
 
     def check_global_request(self, kind, msg):
-        self.log.debug('NullServer.check_port_forward_request()'
-                       .format(kind, msg))
+        self.log.debug('NullServer.check_port_forward_request()')
         return True
 
     def check_channel_direct_tcpip_request(self, chanid, origin, destination):
@@ -413,9 +410,9 @@ class SSHClientTest(unittest.TestCase):
             self.log.info('<<< forward-server received STOP signal')
         except socket.error:
             self.log.critical('{0} sending RST'.format(info))
-        except Exception as e:
-            # we reach this point usually when schan is None (paramiko bug?)
-            self.log.critical(repr(e))
+        # except Exception as e:
+        #     # we reach this point usually when schan is None (paramiko bug?)
+        #     self.log.critical(repr(e))
         finally:
             if schan:
                 self.log.debug('{0} closing connection...'.format(info))
@@ -424,7 +421,7 @@ class SSHClientTest(unittest.TestCase):
                 self.log.debug('{0} connection closed.'.format(info))
 
     def randomize_eport(self):
-        return self.eport + random.randint(1, 999)
+        return random.randint(49152, 65535)
 
     def test_echo_server(self):
         with self._test_server(
@@ -491,7 +488,8 @@ class SSHClientTest(unittest.TestCase):
             remote_bind_address=(self.eaddr, self.eport),
             logger=self.log,
             ssh_config_file=None,
-            allow_agent=False
+            allow_agent=False,
+            host_pkey_directories=[],
         )
         self.assertEqual(server.ssh_host, self.saddr)
         self.assertEqual(server.ssh_port, self.sport)
@@ -693,7 +691,7 @@ class SSHClientTest(unittest.TestCase):
                 open_tunnel(**_kwargs)
                 logged_message = "'{0}' is DEPRECATED use '{1}' instead"\
                     .format(deprecated_arg,
-                            sshtunnel.DEPRECATIONS[deprecated_arg])
+                            sshtunnel._DEPRECATIONS[deprecated_arg])
                 self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
                 self.assertEqual(logged_message, str(w[-1].message))
 
@@ -713,7 +711,7 @@ class SSHClientTest(unittest.TestCase):
                 open_tunnel(**_kwargs)
                 logged_message = "'{0}' is DEPRECATED use '{1}' instead"\
                     .format(deprecated_arg,
-                            sshtunnel.DEPRECATIONS[deprecated_arg])
+                            sshtunnel._DEPRECATIONS[deprecated_arg])
                 self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
                 self.assertEqual(logged_message, str(w[-1].message))
 
@@ -961,8 +959,8 @@ class SSHClientTest(unittest.TestCase):
         ) as server:
             self.assertIsInstance(server.local_bind_addresses, list)
             self.assertListEqual(server.local_bind_addresses,
-                                 [l for l in zip([self.saddr] * 2,
-                                                 server.local_bind_ports)])
+                                 list(zip([self.saddr] * 2,
+                                          server.local_bind_ports)))
             with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError):
                 self.log.info(server.local_bind_address)
 
@@ -1009,7 +1007,8 @@ class SSHClientTest(unittest.TestCase):
                                   '-R', '{0}:{1}'.format(self.eaddr,
                                                          self.eport),
                                   '-c', '',
-                                  '-n'])
+                                  '-n'],
+                            host_pkey_directories=[])
         self.stop_echo_and_ssh_server()
 
     @unittest.skipIf(sys.version_info < (2, 7),
@@ -1084,7 +1083,7 @@ class SSHClientTest(unittest.TestCase):
             s.close
             log = 'send to {0}'.format((self.eaddr, self.eport))
 
-        self.assertTrue(any(log in l for l in
+        self.assertTrue(any(log in msg for msg in
                             self.sshtunnel_log_messages['trace']))
         # set loglevel back to the original value
         logger = sshtunnel.create_logger(logger=self.log,
@@ -1125,7 +1124,8 @@ class SSHClientTest(unittest.TestCase):
             remote_bind_address=(self.eaddr, self.eport),
             logger=self.log,
             ssh_config_file=None,
-            allow_agent=False
+            allow_agent=False,
+            host_pkey_directories=[],
         )
         try:
             tunnel.daemon_forward_servers = case
@@ -1157,11 +1157,10 @@ class SSHClientTest(unittest.TestCase):
             local_bind_address=('', self.randomize_eport()),
             logger=self.log
         ) as server:
-
-            keys = server.get_keys(allow_agent=True)
+            keys = server.get_keys(logger=self.log)
             self.assertIsInstance(keys, list)
-            self.assertTrue(any('keys loaded from agent' in l) for l in
-                            self.sshtunnel_log_messages['info'])
+            self.assertFalse(any('keys loaded from agent' in msg for msg in
+                             self.sshtunnel_log_messages['info']))
 
         with self._test_server(
             (self.saddr, self.sport),
@@ -1171,10 +1170,10 @@ class SSHClientTest(unittest.TestCase):
             local_bind_address=('', self.randomize_eport()),
             logger=self.log
         ) as server:
-            keys = server.get_keys()
+            keys = server.get_keys(logger=self.log, allow_agent=True)
             self.assertIsInstance(keys, list)
-            self.assertFalse(any('keys loaded from agent' in l for l in
-                             self.sshtunnel_log_messages['info']))
+            self.assertTrue(any('keys loaded from agent' in msg for msg in
+                            self.sshtunnel_log_messages['info']))
 
         tmp_dir = tempfile.mkdtemp()
         shutil.copy(get_test_data_path(PKEY_FILE),
@@ -1186,8 +1185,8 @@ class SSHClientTest(unittest.TestCase):
         )
         self.assertIsInstance(keys, list)
         self.assertTrue(
-            any('1 keys loaded from host directory' in l
-                for l in self.sshtunnel_log_messages['info'])
+            any('1 key(s) loaded' in msg
+                for msg in self.sshtunnel_log_messages['info'])
         )
         shutil.rmtree(tmp_dir)
 
@@ -1387,16 +1386,16 @@ class AuxiliaryTest(unittest.TestCase):
                                                              'item',
                                                              kwargs.copy())
 
-    def check_address(self):
+    def test_check_address(self):
         """ Test that an exception is raised with incorrect bind addresses """
         address_list = [('10.0.0.1', 10000),
                         ('10.0.0.1', 10001)]
         if os.name == 'posix':  # UNIX sockets supported by the platform
             address_list.append('/tmp/unix-socket')
+            # UNIX sockets not supported on remote addresses
+            with self.assertRaises(AssertionError):
+                sshtunnel.check_addresses(address_list, is_remote=True)
         self.assertIsNone(sshtunnel.check_addresses(address_list))
-        # UNIX sockets not supported on remote addresses
-        with self.assertRaises(AssertionError):
-            sshtunnel.check_addresses(address_list, is_remote=True)
         with self.assertRaises(ValueError):
             sshtunnel.check_address('this is not valid')
         with self.assertRaises(ValueError):

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.4.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.4.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.4.0.egg-info/entry_points.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.4.0.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.4.0.egg-info/top_level.txt

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.1.4.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.1.4.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.1.4.egg-info/entry_points.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.1.4.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/sshtunnel-0.1.4.egg-info/top_level.txt

No differences were encountered in the control files

More details

Full run details