From 27b800bf3f1c0ee7063221538d2913cec7c048c1 Mon Sep 17 00:00:00 2001 From: Torsten Landschoff Date: Fri, 12 Aug 2011 11:25:36 +0200 Subject: Issue #22: Try IPv4 as well as IPv6 when port is not open on IPv6. With this change, paramiko tries the next address family when the connection gets refused or the target port is unreachable. --- paramiko/client.py | 53 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index c5a2d1ac..da08ad0a 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -25,6 +25,7 @@ import getpass import os import socket import warnings +from errno import ECONNREFUSED, EHOSTUNREACH from paramiko.agent import Agent from paramiko.common import * @@ -231,6 +232,29 @@ class SSHClient (object): """ self._policy = policy + def _families_and_addresses(self, hostname, port): + """ + Yield pairs of address families and addresses to try for connecting. + + @param hostname: the server to connect to + @type hostname: str + @param port: the server port to connect to + @type port: int + @rtype: generator + """ + guess = True + addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) + for (family, socktype, proto, canonname, sockaddr) in addrinfos: + if socktype == socket.SOCK_STREAM: + yield family, sockaddr + guess = False + + # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( + # We only do this if we did not get a single result marked as socktype == SOCK_STREAM. + if guess: + for family, _, _, _, sockaddr in addrinfos: + yield family, sockaddr + def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None, key_filename=None, timeout=None, allow_agent=True, look_for_keys=True, compress=False, sock=None): @@ -288,21 +312,22 @@ class SSHClient (object): @raise socket.error: if a socket error occurred while connecting """ if not sock: - for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): - if socktype == socket.SOCK_STREAM: - af = family - addr = sockaddr - break - else: - # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( - af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) - sock = socket.socket(af, socket.SOCK_STREAM) - if timeout is not None: + for af, addr in self._families_and_addresses(hostname, port): try: - sock.settimeout(timeout) - except: - pass - retry_on_signal(lambda: sock.connect(addr)) + sock = socket.socket(af, socket.SOCK_STREAM) + if timeout is not None: + try: + sock.settimeout(timeout) + except: + pass + retry_on_signal(lambda: sock.connect(addr)) + # Break out of the loop on success + break + except socket.error, e: + # If the port is not open on IPv6 for example, we may still try IPv4. + # Likewise if the host is not reachable using that address family. + if e.errno not in (ECONNREFUSED, EHOSTUNREACH): + raise t = self._transport = Transport(sock) t.use_compression(compress=compress) -- cgit v1.2.3 From d97c938db32c44a8253f6f872cc3b354492a21cc Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 15:33:28 -0800 Subject: Fix docstring for Sphinx --- paramiko/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 312f71a9..dbe7fcba 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -177,11 +177,9 @@ class SSHClient (ClosingContextManager): """ Yield pairs of address families and addresses to try for connecting. - @param hostname: the server to connect to - @type hostname: str - @param port: the server port to connect to - @type port: int - @rtype: generator + :param str hostname: the server to connect to + :param int port: the server port to connect to + :returns: Yields an iterable of ``(family, address)`` tuples """ guess = True addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) -- cgit v1.2.3 From b42b5338de868c3a5343dd03a8ee3f8a968d2f65 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 15:33:32 -0800 Subject: Comment --- paramiko/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/paramiko/client.py b/paramiko/client.py index dbe7fcba..b907d3b0 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -256,6 +256,7 @@ class SSHClient (ClosingContextManager): ``gss_deleg_creds`` and ``gss_host`` arguments. """ if not sock: + # Try multiple possible address families (e.g. IPv4 vs IPv6) for af, addr in self._families_and_addresses(hostname, port): try: sock = socket.socket(af, socket.SOCK_STREAM) -- cgit v1.2.3 From f99e1d8775be20c471c39f8d7a91126b8419381d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 19:33:06 -0800 Subject: Raise usefully ambiguous error when every connect attempt fails. Re #22 --- paramiko/client.py | 24 ++++++++++++++++++++---- paramiko/ssh_exception.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index b907d3b0..57e9919d 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -36,7 +36,9 @@ from paramiko.hostkeys import HostKeys from paramiko.py3compat import string_types from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey -from paramiko.ssh_exception import SSHException, BadHostKeyException +from paramiko.ssh_exception import ( + SSHException, BadHostKeyException, ConnectionError +) from paramiko.transport import Transport from paramiko.util import retry_on_signal, ClosingContextManager @@ -256,8 +258,10 @@ class SSHClient (ClosingContextManager): ``gss_deleg_creds`` and ``gss_host`` arguments. """ if not sock: + errors = {} # Try multiple possible address families (e.g. IPv4 vs IPv6) - for af, addr in self._families_and_addresses(hostname, port): + to_try = list(self._families_and_addresses(hostname, port)) + for af, addr in to_try: try: sock = socket.socket(af, socket.SOCK_STREAM) if timeout is not None: @@ -269,10 +273,22 @@ class SSHClient (ClosingContextManager): # Break out of the loop on success break except socket.error, e: - # If the port is not open on IPv6 for example, we may still try IPv4. - # Likewise if the host is not reachable using that address family. + # Raise anything that isn't a straight up connection error + # (such as a resolution error) if e.errno not in (ECONNREFUSED, EHOSTUNREACH): raise + # Capture anything else so we know how the run looks once + # iteration is complete. Retain info about which attempt + # this was. + errors[addr] = e + + # Make sure we explode usefully if no address family attempts + # succeeded. We've no way of knowing which error is the "right" + # one, so we construct a hybrid exception containing all the real + # ones, of a subclass that client code should still be watching for + # (socket.error) + if len(errors) == len(to_try): + raise ConnectionError(errors) t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds) t.use_compression(compress=compress) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index b99e42b3..7e6f2568 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -16,6 +16,8 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +import socket + class SSHException (Exception): """ @@ -129,3 +131,31 @@ class ProxyCommandFailure (SSHException): self.error = error # for unpickling self.args = (command, error, ) + + +class ConnectionError(socket.error): + """ + High-level socket error wrapping 1+ actual socket.error objects. + + To see the wrapped exception objects, access the ``errors`` attribute. + ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', + 22)``) and whose values are the exception encountered trying to connect to + that address. + + It is implied/assumed that all the errors given to a single instance of + this class are from connecting to the same hostname + port (and thus that + the differences are in the resolution of the hostname - e.g. IPv4 vs v6). + """ + def __init__(self, errors): + """ + :param dict errors: + The errors dict to store, as described by class docstring. + """ + addrs = errors.keys() + body = ', '.join([x[0] for x in addrs[:-1]]) + tail = addrs[-1][0] + msg = "Unable to connect to port {0} at {1} or {2}" + super(ConnectionError, self).__init__( + msg.format(addrs[0][1], body, tail) + ) + self.errors = errors -- cgit v1.2.3 From 4ca8d68c0443c4e5e17ae4fcee39dd6f2507c7cd Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 27 Feb 2015 13:19:35 -0800 Subject: Changelog closes #22 --- sites/www/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 6520dde4..0e8f92c4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :bug:`22 major` Try harder to connect to multiple network families (e.g. IPv4 + vs IPv6) in case of connection issues; this helps with problems such as hosts + which resolve both IPv4 and IPv6 addresses but are only listening on IPv4. + Thanks to Dries Desmet for original report and Torsten Landschoff for the + foundational patchset. * :bug:`402` Check to see if an SSH agent is actually present before trying to forward it to the remote end. This replaces what was usually a useless ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken -- cgit v1.2.3