Sort resolved IP addresses for dashboard (#8536)

Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
David Woodhouse 2025-04-17 02:19:55 +01:00 committed by GitHub
parent 4a65fd76b3
commit 3c7bb65a23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 3 deletions

View File

@ -38,7 +38,7 @@ import yaml
from yaml.nodes import Node
from esphome import const, platformio_api, yaml_util
from esphome.helpers import get_bool_env, mkdir_p
from esphome.helpers import get_bool_env, mkdir_p, sort_ip_addresses
from esphome.storage_json import (
StorageJSON,
archive_storage_path,
@ -336,7 +336,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
# Use the IP address if available but only
# if the API is loaded and the device is online
# since MQTT logging will not work otherwise
port = address_list[0]
port = sort_ip_addresses(address_list)[0]
elif (
entry.address
and (
@ -347,7 +347,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
and not isinstance(address_list, Exception)
):
# If mdns is not available, try to use the DNS cache
port = address_list[0]
port = sort_ip_addresses(address_list)[0]
return [
*DASHBOARD_COMMAND,

View File

@ -200,6 +200,45 @@ def resolve_ip_address(host, port):
return res
def sort_ip_addresses(address_list: list[str]) -> list[str]:
"""Takes a list of IP addresses in string form, e.g. from mDNS or MQTT,
and sorts them into the best order to actually try connecting to them.
This is roughly based on RFC6724 but a lot simpler: First we choose
IPv6 addresses, then Legacy IP addresses, and lowest priority is
link-local IPv6 addresses that don't have a link specified (which
are useless, but mDNS does provide them in that form). Addresses
which cannot be parsed are silently dropped.
"""
import socket
# First "resolve" all the IP addresses to getaddrinfo() tuples of the form
# (family, type, proto, canonname, sockaddr)
res: list[
tuple[
int,
int,
int,
Union[str, None],
Union[tuple[str, int], tuple[str, int, int, int]],
]
] = []
for addr in address_list:
# This should always work as these are supposed to be IP addresses
try:
res += socket.getaddrinfo(
addr, 0, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST
)
except OSError:
_LOGGER.info("Failed to parse IP address '%s'", addr)
# Now use that information to sort them.
res.sort(key=addr_preference_)
# Finally, turn the getaddrinfo() tuples back into plain hostnames.
return [socket.getnameinfo(r[4], socket.NI_NUMERICHOST)[0] for r in res]
def get_bool_env(var, default=False):
value = os.getenv(var, default)
if isinstance(value, str):

View File

@ -267,3 +267,13 @@ def test_sanitize(text, expected):
actual = helpers.sanitize(text)
assert actual == expected
@pytest.mark.parametrize(
"text, expected",
((["127.0.0.1", "fe80::1", "2001::2"], ["2001::2", "127.0.0.1", "fe80::1"]),),
)
def test_sort_ip_addresses(text: list[str], expected: list[str]) -> None:
actual = helpers.sort_ip_addresses(text)
assert actual == expected