Skip to content

Commit 7132625

Browse files
authored
feat(util): fail early when hostname is not resolvable in is_resolvable (#6772)
Building on the previous optimization in #2040, add an additional optimization to fail early when the hostname is not resolvable at all.
1 parent 4db78c9 commit 7132625

3 files changed

Lines changed: 76 additions & 13 deletions

File tree

cloudinit/util.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,10 @@ def is_resolvable(url) -> bool:
13001300
with suppress(ValueError):
13011301
if net.is_ip_address(parsed_url.netloc.strip("[]")):
13021302
return True
1303+
try:
1304+
hostname_result = socket.getaddrinfo(name, None)
1305+
except (socket.gaierror, socket.error):
1306+
return False
13031307

13041308
if _DNS_REDIRECT_IP is None:
13051309
badips = set()
@@ -1324,13 +1328,11 @@ def is_resolvable(url) -> bool:
13241328
if badresults:
13251329
LOG.debug("detected dns redirection: %s", badresults)
13261330

1327-
try:
1328-
result = socket.getaddrinfo(name, None)
1329-
# check first result's sockaddr field
1330-
addr = result[0][4][0]
1331-
return addr not in _DNS_REDIRECT_IP
1332-
except (socket.gaierror, socket.error):
1331+
# check first result's sockaddr field
1332+
addr = hostname_result[0][4][0]
1333+
if addr in _DNS_REDIRECT_IP:
13331334
return False
1335+
return True
13341336

13351337

13361338
def get_hostname():

tests/unittests/config/test_apt_source_v3.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -927,20 +927,32 @@ def test_apt_v3_url_resolvable(self):
927927
# former tests can leave this set (or not if the test is ran directly)
928928
# do a hard reset to ensure a stable result
929929
util._DNS_REDIRECT_IP = None
930+
badnames = (
931+
"does-not-exist.example.com.",
932+
"example.invalid.",
933+
"__cloud_init_expected_not_found__",
934+
)
930935
bad = [(None, None, None, "badname", ["10.3.2.1"])]
931936
good = [(None, None, None, "goodname", ["10.2.3.4"])]
932937
with mock.patch.object(
933-
socket, "getaddrinfo", side_effect=[bad, bad, bad, good, good]
938+
socket, "getaddrinfo", side_effect=[good, bad, bad, bad]
934939
) as mocksock:
935940
ret = util.is_resolvable_url("http://us.archive.ubuntu.com/ubuntu")
936-
ret2 = util.is_resolvable_url("http://1.2.3.4/ubuntu")
937-
mocksock.assert_any_call(
938-
"does-not-exist.example.com.", None, 0, 0, 1, 2
939-
)
940-
mocksock.assert_any_call("example.invalid.", None, 0, 0, 1, 2)
941+
for badname in badnames:
942+
mocksock.assert_any_call(badname, None, 0, 0, 1, 2)
941943
mocksock.assert_any_call("us.archive.ubuntu.com", None)
942-
943944
assert ret is True
945+
946+
# IP addresses skip DNS checks entirely
947+
with mock.patch.object(socket, "getaddrinfo") as mocksock:
948+
ret2 = util.is_resolvable_url("http://1.2.3.4/ubuntu")
949+
mocksock.assert_not_called()
950+
# Verify badnames were NOT checked for IP addresses
951+
for badname in badnames:
952+
assert (
953+
mock.call(badname, None, 0, 0, 1, 2)
954+
not in mocksock.call_args_list
955+
)
944956
assert ret2 is True
945957

946958
# side effect need only bad ret after initial call
@@ -952,6 +964,15 @@ def test_apt_v3_url_resolvable(self):
952964
mocksock.assert_has_calls(calls)
953965
assert ret3 is False
954966

967+
# Test unresolvable hostname
968+
with mock.patch.object(
969+
socket, "getaddrinfo", side_effect=[bad]
970+
) as mocksock:
971+
ret4 = util.is_resolvable_url("http://instance.:3336")
972+
calls = [call("instance.", None)]
973+
mocksock.assert_has_calls(calls)
974+
assert ret4 is False
975+
955976
def test_apt_v3_disable_suites(self):
956977
"""test_disable_suites - disable_suites with many configurations"""
957978
release = "xenial"

tests/unittests/test_util.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3173,6 +3173,12 @@ def test_from_str(self, str_ver, cls_ver):
31733173

31743174
@pytest.mark.allow_dns_lookup
31753175
class TestResolvable:
3176+
@pytest.fixture(autouse=True)
3177+
def reset_dns_redirect_ip(self):
3178+
util._DNS_REDIRECT_IP = None
3179+
yield # Test runs here
3180+
util._DNS_REDIRECT_IP = None
3181+
31763182
@mock.patch.object(util, "_DNS_REDIRECT_IP", return_value=True)
31773183
@mock.patch.object(util.socket, "getaddrinfo")
31783184
def test_ips_need_not_be_resolved(self, m_getaddr, m_dns):
@@ -3185,6 +3191,40 @@ def test_ips_need_not_be_resolved(self, m_getaddr, m_dns):
31853191
assert util.is_resolvable("http://[fd00:ec2::254]/") is True
31863192
assert not m_getaddr.called
31873193

3194+
@mock.patch.object(util.net, "is_ip_address")
3195+
@mock.patch.object(util.socket, "getaddrinfo")
3196+
def test_hostnames_require_dns_resolution(self, m_getaddr, m_is_ip):
3197+
"""Hostnames should go through DNS resolution."""
3198+
m_is_ip.return_value = False
3199+
3200+
def mock_getaddrinfo(host, port, *args, **kwargs):
3201+
badnames = (
3202+
"does-not-exist.example.com.",
3203+
"example.invalid.",
3204+
"__cloud_init_expected_not_found__",
3205+
)
3206+
if host in badnames:
3207+
return [(None, None, None, "badname", ("192.0.2.1", 0))]
3208+
return [(None, None, None, "example.com", ("10.2.3.4", 0))]
3209+
3210+
m_getaddr.side_effect = mock_getaddrinfo
3211+
3212+
assert util.is_resolvable("http://example.com/") is True
3213+
assert m_getaddr.called
3214+
3215+
assert m_getaddr.call_args_list[0] == mock.call("example.com", None)
3216+
3217+
badnames = (
3218+
"does-not-exist.example.com.",
3219+
"example.invalid.",
3220+
"__cloud_init_expected_not_found__",
3221+
)
3222+
called_hosts = [call[0][0] for call in m_getaddr.call_args_list[1:]]
3223+
for badname in badnames:
3224+
assert (
3225+
badname in called_hosts
3226+
), f"Expected badname {badname} to be checked"
3227+
31883228

31893229
class TestMaybeB64Decode:
31903230
"""Test the maybe_b64decode helper function."""

0 commit comments

Comments
 (0)