Remove hard dependency on netifaces

The project was archived in 2021, and we can fairly easily replace it
with some ctypes code to call getifaddrs ourselves.

Be willing to fall back to netifaces (with a warning) in case getifaddrs
is not available, but I'm fairly certain it will be for all platforms we
support.

Could maybe use some more testing on big-endian arches / BSDs, but an
attempt was at least made at supporting them.

Partial-Bug: #2019233
Change-Id: I1189a60204cf96c291619f8d8ec957ed8a5be1ce
This commit is contained in:
Tim Burke 2023-05-22 10:37:12 -07:00
parent e29e2c3ae5
commit 23fa18d302
3 changed files with 172 additions and 4 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)