Repository: urllib3/urllib3

Issue:
urllib3 attempts to use IPv6 even when IPv6 is disabled
This is an issue when running on a server without IPv6 (must be disabled because the network does not support it). Example when connecting to https://graph.facebook.com using requests and IPv4 happens to fail:

```
HTTPSConnectionPool(host='graph.facebook.com', port=443): Max retries exceeded with url: /v2.5/me/feed (Caused by NewConnectionError('<requests.packages.urllib3.connection.VerifiedHTTPSConnection object at 0x7f4dbd158518>: Failed to establish a new connection: [Errno 97] Address family not supported by protocol',))
Traceback (most recent call last):
  File "/home/lib/python3.4/site-packages/requests/packages/urllib3/connection.py", line 137, in _new_conn
    (self.host, self.port), self.timeout, **extra_kw)
  File "/home/lib/python3.4/site-packages/requests/packages/urllib3/util/connection.py", line 91, in create_connection
    raise err
  File "/home/lib/python3.4/site-packages/requests/packages/urllib3/util/connection.py", line 71, in create_connection
    sock = socket.socket(af, socktype, proto)
  File "/usr/lib/python3.4/socket.py", line 126, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
OSError: [Errno 97] Address family not supported by protocol
```

urllib3 should throw an exception after exhausting all IPv4 options instead of trying (and invariably failing) IPv6.

See closed issue https://github.com/kennethreitz/requests/issues/3084.



Discussion:

Resolving patch:
diff --git a/urllib3/util/connection.py b/urllib3/util/connection.py
--- a/urllib3/util/connection.py
+++ b/urllib3/util/connection.py
@@ -46,6 +46,8 @@ def is_connection_dropped(conn):  # Platform-specific
 
 # This function is copied from socket.py in the Python 2.7 standard
 # library test suite. Added to its signature is only `socket_options`.
+# One additional modification is that we avoid binding to IPv6 servers
+# discovered in DNS if the system doesn't have IPv6 functionality.
 def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
                       source_address=None, socket_options=None):
     """Connect to *address* and return the socket object.
@@ -64,14 +66,19 @@ def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
     if host.startswith('['):
         host = host.strip('[]')
     err = None
-    for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
+
+    # Using the value from allowed_gai_family() in the context of getaddrinfo lets
+    # us select whether to work with IPv4 DNS records, IPv6 records, or both.
+    # The original create_connection function always returns all records.
+    family = allowed_gai_family()
+
+    for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
         af, socktype, proto, canonname, sa = res
         sock = None
         try:
             sock = socket.socket(af, socktype, proto)
 
             # If provided, set socket level options before connecting.
-            # This is the only addition urllib3 makes to this function.
             _set_socket_options(sock, socket_options)
 
             if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
@@ -99,3 +106,39 @@ def _set_socket_options(sock, options):
 
     for opt in options:
         sock.setsockopt(*opt)
+
+
+def allowed_gai_family():
+    """This function is designed to work in the context of
+    getaddrinfo, where family=socket.AF_UNSPEC is the default and
+    will perform a DNS search for both IPv6 and IPv4 records."""
+
+    family = socket.AF_INET
+    if HAS_IPV6:
+        family = socket.AF_UNSPEC
+    return family
+
+
+def _has_ipv6(host):
+    """ Returns True if the system can bind an IPv6 address. """
+    sock = None
+    has_ipv6 = False
+
+    if socket.has_ipv6:
+        # has_ipv6 returns true if cPython was compiled with IPv6 support.
+        # It does not tell us if the system has IPv6 support enabled. To
+        # determine that we must bind to an IPv6 address.
+        # https://github.com/shazow/urllib3/pull/611
+        # https://bugs.python.org/issue658327
+        try:
+            sock = socket.socket(socket.AF_INET6)
+            sock.bind((host, 0))
+            has_ipv6 = True
+        except Exception:
+            pass
+
+    if sock:
+        sock.close()
+    return has_ipv6
+
+HAS_IPV6 = _has_ipv6('::1')


Test patch:
diff --git a/test/test_util.py b/test/test_util.py
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -3,6 +3,7 @@
 import logging
 import unittest
 import ssl
+import socket
 from itertools import chain
 
 from mock import patch, Mock
@@ -28,7 +29,10 @@
     SSLError,
     SNIMissingWarning,
 )
-
+from urllib3.util.connection import (
+    allowed_gai_family,
+    _has_ipv6
+)
 from urllib3.util import is_fp_closed, ssl_
 
 from . import clear_warnings
@@ -442,3 +446,29 @@ def test_const_compare_digest_fallback(self):
 
         incorrect = hashlib.sha256(b'xyz').digest()
         self.assertFalse(_const_compare_digest_backport(target, incorrect))
+
+    def test_has_ipv6_disabled_on_compile(self):
+        with patch('socket.has_ipv6', False):
+            self.assertFalse(_has_ipv6('::1'))
+
+    def test_has_ipv6_enabled_but_fails(self):
+        with patch('socket.has_ipv6', True):
+            with patch('socket.socket') as mock:
+                instance = mock.return_value
+                instance.bind = Mock(side_effect=Exception('No IPv6 here!'))
+                self.assertFalse(_has_ipv6('::1'))
+
+    def test_has_ipv6_enabled_and_working(self):
+        with patch('socket.has_ipv6', True):
+            with patch('socket.socket') as mock:
+                instance = mock.return_value
+                instance.bind.return_value = True
+                self.assertTrue(_has_ipv6('::1'))
+
+    def test_ip_family_ipv6_enabled(self):
+        with patch('urllib3.util.connection.HAS_IPV6', True):
+            self.assertEqual(allowed_gai_family(), socket.AF_UNSPEC)
+
+    def test_ip_family_ipv6_disabled(self):
+        with patch('urllib3.util.connection.HAS_IPV6', False):
+            self.assertEqual(allowed_gai_family(), socket.AF_INET)