Fix various zeroconf IPv6 compatibility issues (#53505)
parent
64a3c669ce
commit
ce663f629c
|
@ -150,14 +150,21 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
|||
if not adapter["enabled"]:
|
||||
continue
|
||||
if ipv4s := adapter["ipv4"]:
|
||||
interfaces.append(ipv4s[0]["address"])
|
||||
elif ipv6s := adapter["ipv6"]:
|
||||
interfaces.append(ipv6s[0]["scope_id"])
|
||||
interfaces.extend(
|
||||
ipv4["address"]
|
||||
for ipv4 in ipv4s
|
||||
if not ipaddress.ip_address(ipv4["address"]).is_loopback
|
||||
)
|
||||
if adapter["ipv6"]:
|
||||
ifi = socket.if_nametoindex(adapter["name"])
|
||||
interfaces.append(ifi)
|
||||
|
||||
ipv6 = True
|
||||
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
|
||||
ipv6 = False
|
||||
zc_args["ip_version"] = IPVersion.V4Only
|
||||
else:
|
||||
zc_args["ip_version"] = IPVersion.All
|
||||
|
||||
aio_zc = await _async_get_instance(hass, **zc_args)
|
||||
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
||||
|
@ -190,6 +197,32 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def _get_announced_addresses(
|
||||
adapters: list[Adapter],
|
||||
first_ip: bytes | None = None,
|
||||
) -> list[bytes]:
|
||||
"""Return a list of IP addresses to announce via zeroconf.
|
||||
|
||||
If first_ip is not None, it will be the first address in the list.
|
||||
"""
|
||||
addresses = {
|
||||
addr.packed
|
||||
for addr in [
|
||||
ipaddress.ip_address(ip["address"])
|
||||
for adapter in adapters
|
||||
if adapter["enabled"]
|
||||
for ip in cast(list, adapter["ipv6"]) + cast(list, adapter["ipv4"])
|
||||
]
|
||||
if not (addr.is_unspecified or addr.is_loopback)
|
||||
}
|
||||
if first_ip:
|
||||
address_list = [first_ip]
|
||||
address_list.extend(addresses - set({first_ip}))
|
||||
else:
|
||||
address_list = list(addresses)
|
||||
return address_list
|
||||
|
||||
|
||||
async def _async_register_hass_zc_service(
|
||||
hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str
|
||||
) -> None:
|
||||
|
@ -218,12 +251,15 @@ async def _async_register_hass_zc_service(
|
|||
# Set old base URL based on external or internal
|
||||
params["base_url"] = params["external_url"] or params["internal_url"]
|
||||
|
||||
host_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP)
|
||||
adapters = await network.async_get_adapters(hass)
|
||||
|
||||
try:
|
||||
# Puts the default IPv4 address first in the list to preserve compatibility,
|
||||
# because some mDNS implementations ignores anything but the first announced address.
|
||||
host_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP)
|
||||
host_ip_pton = None
|
||||
if host_ip:
|
||||
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
|
||||
except OSError:
|
||||
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
|
||||
address_list = _get_announced_addresses(adapters, host_ip_pton)
|
||||
|
||||
_suppress_invalid_properties(params)
|
||||
|
||||
|
@ -231,7 +267,7 @@ async def _async_register_hass_zc_service(
|
|||
ZEROCONF_TYPE,
|
||||
name=f"{valid_location_name}.{ZEROCONF_TYPE}",
|
||||
server=f"{uuid}.local.",
|
||||
addresses=[host_ip_pton],
|
||||
addresses=address_list,
|
||||
port=hass.http.server_port,
|
||||
properties=params,
|
||||
)
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
"""Test Zeroconf component setup process."""
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange
|
||||
from zeroconf.asyncio import AsyncServiceInfo
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6
|
||||
from homeassistant.components.zeroconf import (
|
||||
CONF_DEFAULT_INTERFACE,
|
||||
CONF_IPV6,
|
||||
_get_announced_addresses,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
|
@ -726,10 +731,16 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
|
|||
"ipv6": [
|
||||
{
|
||||
"address": "2001:db8::",
|
||||
"network_prefix": 8,
|
||||
"network_prefix": 64,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "fe80::1234:5678:9abc:def0",
|
||||
"network_prefix": 64,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 1,
|
||||
},
|
||||
],
|
||||
"name": "eth0",
|
||||
},
|
||||
|
@ -741,6 +752,21 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
|
|||
"ipv6": [],
|
||||
"name": "eth1",
|
||||
},
|
||||
{
|
||||
"auto": True,
|
||||
"default": False,
|
||||
"enabled": True,
|
||||
"ipv4": [{"address": "172.16.1.5", "network_prefix": 23}],
|
||||
"ipv6": [
|
||||
{
|
||||
"address": "fe80::dead:beef:dead:beef",
|
||||
"network_prefix": 64,
|
||||
"flowinfo": 1,
|
||||
"scope_id": 3,
|
||||
}
|
||||
],
|
||||
"name": "eth2",
|
||||
},
|
||||
{
|
||||
"auto": False,
|
||||
"default": False,
|
||||
|
@ -764,9 +790,36 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero
|
|||
), patch(
|
||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
||||
side_effect=get_service_info_mock,
|
||||
), patch(
|
||||
"socket.if_nametoindex",
|
||||
side_effect=lambda iface: {"eth0": 1, "eth1": 2, "eth2": 3, "vtun0": 4}.get(
|
||||
iface, 0
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_zc.mock_calls[0] == call(
|
||||
interfaces=[1, "192.168.1.5", "172.16.1.5", 3], ip_version=IPVersion.All
|
||||
)
|
||||
|
||||
assert mock_zc.mock_calls[0] == call(interfaces=[1, "192.168.1.5"])
|
||||
|
||||
async def test_get_announced_addresses(hass, mock_async_zeroconf):
|
||||
"""Test addresses for mDNS announcement."""
|
||||
expected = {
|
||||
ip_address(ip).packed
|
||||
for ip in [
|
||||
"fe80::1234:5678:9abc:def0",
|
||||
"2001:db8::",
|
||||
"192.168.1.5",
|
||||
"fe80::dead:beef:dead:beef",
|
||||
"172.16.1.5",
|
||||
]
|
||||
}
|
||||
first_ip = ip_address("172.16.1.5").packed
|
||||
actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip)
|
||||
assert actual[0] == first_ip and set(actual) == expected
|
||||
|
||||
first_ip = ip_address("192.168.1.5").packed
|
||||
actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip)
|
||||
assert actual[0] == first_ip and set(actual) == expected
|
||||
|
|
Loading…
Reference in New Issue