Fix various zeroconf IPv6 compatibility issues (#53505)

pull/53575/head
Yuxiang Zhu 2021-07-28 01:31:51 +08:00 committed by GitHub
parent 64a3c669ce
commit ce663f629c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 101 additions and 12 deletions

View File

@ -150,14 +150,21 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
if not adapter["enabled"]: if not adapter["enabled"]:
continue continue
if ipv4s := adapter["ipv4"]: if ipv4s := adapter["ipv4"]:
interfaces.append(ipv4s[0]["address"]) interfaces.extend(
elif ipv6s := adapter["ipv6"]: ipv4["address"]
interfaces.append(ipv6s[0]["scope_id"]) 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 ipv6 = True
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
ipv6 = False ipv6 = False
zc_args["ip_version"] = IPVersion.V4Only zc_args["ip_version"] = IPVersion.V4Only
else:
zc_args["ip_version"] = IPVersion.All
aio_zc = await _async_get_instance(hass, **zc_args) aio_zc = await _async_get_instance(hass, **zc_args)
zeroconf = cast(HaZeroconf, aio_zc.zeroconf) zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
@ -190,6 +197,32 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
return True 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( async def _async_register_hass_zc_service(
hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str
) -> None: ) -> None:
@ -218,12 +251,15 @@ async def _async_register_hass_zc_service(
# Set old base URL based on external or internal # Set old base URL based on external or internal
params["base_url"] = params["external_url"] or params["internal_url"] 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) host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
except OSError: address_list = _get_announced_addresses(adapters, host_ip_pton)
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
_suppress_invalid_properties(params) _suppress_invalid_properties(params)
@ -231,7 +267,7 @@ async def _async_register_hass_zc_service(
ZEROCONF_TYPE, ZEROCONF_TYPE,
name=f"{valid_location_name}.{ZEROCONF_TYPE}", name=f"{valid_location_name}.{ZEROCONF_TYPE}",
server=f"{uuid}.local.", server=f"{uuid}.local.",
addresses=[host_ip_pton], addresses=address_list,
port=hass.http.server_port, port=hass.http.server_port,
properties=params, properties=params,
) )

View File

@ -1,11 +1,16 @@
"""Test Zeroconf component setup process.""" """Test Zeroconf component setup process."""
from ipaddress import ip_address
from unittest.mock import call, patch from unittest.mock import call, patch
from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange
from zeroconf.asyncio import AsyncServiceInfo from zeroconf.asyncio import AsyncServiceInfo
from homeassistant.components import zeroconf 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 ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
@ -726,10 +731,16 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
"ipv6": [ "ipv6": [
{ {
"address": "2001:db8::", "address": "2001:db8::",
"network_prefix": 8, "network_prefix": 64,
"flowinfo": 1, "flowinfo": 1,
"scope_id": 1, "scope_id": 1,
} },
{
"address": "fe80::1234:5678:9abc:def0",
"network_prefix": 64,
"flowinfo": 1,
"scope_id": 1,
},
], ],
"name": "eth0", "name": "eth0",
}, },
@ -741,6 +752,21 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
"ipv6": [], "ipv6": [],
"name": "eth1", "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, "auto": False,
"default": False, "default": False,
@ -764,9 +790,36 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero
), patch( ), patch(
"homeassistant.components.zeroconf.AsyncServiceInfo", "homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock, 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: {}}) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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