diff --git a/requirements.txt b/requirements.txt index 3b74c3a874..e9d54faaf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ eventlet>=0.25.0 # MIT greenlet>=0.3.2 -netifaces>=0.8,!=0.10.0,!=0.10.1 PasteDeploy>=2.0.0 lxml>=3.4.1 requests>=2.14.2 # Apache-2.0 diff --git a/swift/common/utils/ipaddrs.py b/swift/common/utils/ipaddrs.py index 8375a0a1f4..82a3b8a739 100644 --- a/swift/common/utils/ipaddrs.py +++ b/swift/common/utils/ipaddrs.py @@ -13,9 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import netifaces +import ctypes +import ctypes.util +import os +import platform import re import socket +import warnings # Used by the parse_socket_string() function to validate IPv6 addresses @@ -62,6 +66,83 @@ def expand_ipv6(address): return socket.inet_ntop(socket.AF_INET6, packed_ip) +libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) +try: + getifaddrs = libc.getifaddrs + freeifaddrs = libc.freeifaddrs + netifaces = None # for patching +except AttributeError: + getifaddrs = None + freeifaddrs = None + try: + import netifaces + except ImportError: + raise ImportError('C function getifaddrs not available, ' + 'and netifaces not installed') + else: + warnings.warn('getifaddrs is not available; falling back to the ' + 'archived and no longer maintained netifaces project. ' + 'This fallback will be removed in a future release; ' + 'see https://bugs.launchpad.net/swift/+bug/2019233 for ' + 'more information.', FutureWarning) +else: + class sockaddr_in4(ctypes.Structure): + if platform.system() == 'Linux': + _fields_ = [ + ("sin_family", ctypes.c_uint16), + ("sin_port", ctypes.c_uint16), + ("sin_addr", ctypes.c_ubyte * 4), + ] + else: + # Assume BSD / OS X + _fields_ = [ + ("sin_len", ctypes.c_uint8), + ("sin_family", ctypes.c_uint8), + ("sin_port", ctypes.c_uint16), + ("sin_addr", ctypes.c_ubyte * 4), + ] + + class sockaddr_in6(ctypes.Structure): + if platform.system() == 'Linux': + _fields_ = [ + ("sin6_family", ctypes.c_uint16), + ("sin6_port", ctypes.c_uint16), + ("sin6_flowinfo", ctypes.c_uint32), + ("sin6_addr", ctypes.c_ubyte * 16), + ] + else: + # Assume BSD / OS X + _fields_ = [ + ("sin6_len", ctypes.c_uint8), + ("sin6_family", ctypes.c_uint8), + ("sin6_port", ctypes.c_uint16), + ("sin6_flowinfo", ctypes.c_uint32), + ("sin6_addr", ctypes.c_ubyte * 16), + ] + + class ifaddrs(ctypes.Structure): + pass + + # Have to do this a little later so we can self-reference + ifaddrs._fields_ = [ + ("ifa_next", ctypes.POINTER(ifaddrs)), + ("ifa_name", ctypes.c_char_p), + ("ifa_flags", ctypes.c_int), + # Use the smaller of the two to start, can cast later + # when we *know* we're looking at INET6 + ("ifa_addr", ctypes.POINTER(sockaddr_in4)), + # Don't care about the rest of the fields + ] + + def errcheck(result, func, arguments): + if result != 0: + errno = ctypes.set_errno(0) + raise OSError(errno, "getifaddrs: %s" % os.strerror(errno)) + return result + + getifaddrs.errcheck = errcheck + + def whataremyips(ring_ip=None): """ Get "our" IP addresses ("us" being the set of services configured by @@ -85,6 +166,40 @@ def whataremyips(ring_ip=None): pass addresses = [] + + if getifaddrs: + addrs = ctypes.POINTER(ifaddrs)() + getifaddrs(ctypes.byref(addrs)) + try: + cur = addrs + while cur: + if not cur.contents.ifa_addr: + # Not all interfaces will have addresses; move on + cur = cur.contents.ifa_next + continue + sa_family = cur.contents.ifa_addr.contents.sin_family + if sa_family == socket.AF_INET: + addresses.append( + socket.inet_ntop( + socket.AF_INET, + cur.contents.ifa_addr.contents.sin_addr, + ) + ) + elif sa_family == socket.AF_INET6: + addr = ctypes.cast(cur.contents.ifa_addr, + ctypes.POINTER(sockaddr_in6)) + addresses.append( + socket.inet_ntop( + socket.AF_INET6, + addr.contents.sin6_addr, + ) + ) + cur = cur.contents.ifa_next + finally: + freeifaddrs(addrs) + return addresses + + # getifaddrs not available; try netifaces for interface in netifaces.interfaces(): try: iface_data = netifaces.ifaddresses(interface) diff --git a/test/unit/common/utils/test_ipaddrs.py b/test/unit/common/utils/test_ipaddrs.py index 3d49c595bf..f4a0bd636d 100644 --- a/test/unit/common/utils/test_ipaddrs.py +++ b/test/unit/common/utils/test_ipaddrs.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import ctypes from mock import patch import socket import unittest @@ -125,6 +126,57 @@ class TestWhatAreMyIPs(unittest.TestCase): def test_whataremyips_bind_ip_specific(self): self.assertEqual(['1.2.3.4'], utils.whataremyips('1.2.3.4')) + def test_whataremyips_getifaddrs(self): + def mock_getifaddrs(ptr): + addrs = [ + utils_ipaddrs.ifaddrs(None, b'lo', 0, ctypes.pointer( + utils_ipaddrs.sockaddr_in4( + sin_family=socket.AF_INET, + sin_addr=(127, 0, 0, 1)))), + utils_ipaddrs.ifaddrs(None, b'lo', 0, ctypes.cast( + ctypes.pointer(utils_ipaddrs.sockaddr_in6( + sin6_family=socket.AF_INET6, + sin6_addr=( + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1))), + ctypes.POINTER(utils_ipaddrs.sockaddr_in4))), + utils_ipaddrs.ifaddrs(None, b'eth0', 0, ctypes.pointer( + utils_ipaddrs.sockaddr_in4( + sin_family=socket.AF_INET, + sin_addr=(192, 168, 50, 63)))), + utils_ipaddrs.ifaddrs(None, b'eth0', 0, ctypes.cast( + ctypes.pointer(utils_ipaddrs.sockaddr_in6( + sin6_family=socket.AF_INET6, + sin6_addr=( + 254, 128, 0, 0, 0, 0, 0, 0, + 106, 191, 199, 168, 109, 243, 41, 35))), + ctypes.POINTER(utils_ipaddrs.sockaddr_in4))), + # MAC address will be ignored + utils_ipaddrs.ifaddrs(None, b'eth0', 0, ctypes.cast( + ctypes.pointer(utils_ipaddrs.sockaddr_in6( + sin6_family=getattr(socket, 'AF_PACKET', 17), + sin6_port=0, + sin6_flowinfo=2, + sin6_addr=( + 1, 0, 0, 6, 172, 116, 177, 85, + 64, 146, 0, 0, 0, 0, 0, 0))), + ctypes.POINTER(utils_ipaddrs.sockaddr_in4))), + # Seen in the wild: no addresses at all + utils_ipaddrs.ifaddrs(None, b'cscotun0', 69841), + ] + for cur, nxt in zip(addrs, addrs[1:]): + cur.ifa_next = ctypes.pointer(nxt) + ptr._obj.contents = addrs[0] + + with patch.object(utils_ipaddrs, 'getifaddrs', mock_getifaddrs), \ + patch('swift.common.utils.ipaddrs.freeifaddrs') as mock_free: + self.assertEqual(utils.whataremyips(), [ + '127.0.0.1', + '::1', + '192.168.50.63', + 'fe80::6abf:c7a8:6df3:2923', + ]) + self.assertEqual(len(mock_free.mock_calls), 1) + def test_whataremyips_netifaces_error(self): class FakeNetifaces(object): @staticmethod @@ -135,7 +187,8 @@ class TestWhatAreMyIPs(unittest.TestCase): def ifaddresses(interface): raise ValueError - with patch.object(utils_ipaddrs, 'netifaces', FakeNetifaces): + with patch.object(utils_ipaddrs, 'getifaddrs', None), \ + patch.object(utils_ipaddrs, 'netifaces', FakeNetifaces): self.assertEqual(utils.whataremyips(), []) def test_whataremyips_netifaces_ipv6(self): @@ -156,7 +209,8 @@ class TestWhatAreMyIPs(unittest.TestCase): {'netmask': 'ffff:ffff:ffff:ffff::', 'addr': '%s%%%s' % (test_ipv6_address, test_interface)}]} - with patch.object(utils_ipaddrs, 'netifaces', FakeNetifaces): + with patch.object(utils_ipaddrs, 'getifaddrs', None), \ + patch.object(utils_ipaddrs, 'netifaces', FakeNetifaces): myips = utils.whataremyips() self.assertEqual(len(myips), 1) self.assertEqual(myips[0], test_ipv6_address)