Avoid enabling ipv6 dual stack for zeroconf on unsupported platforms (#56584)

This commit is contained in:
J. Nick Koston 2021-09-26 11:51:34 -05:00 committed by GitHub
parent f74291ccb6
commit 26f73779cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 96 additions and 18 deletions

View File

@ -5,9 +5,10 @@ import asyncio
from collections.abc import Coroutine from collections.abc import Coroutine
from contextlib import suppress from contextlib import suppress
import fnmatch import fnmatch
from ipaddress import IPv6Address, ip_address from ipaddress import IPv4Address, IPv6Address, ip_address
import logging import logging
import socket import socket
import sys
from typing import Any, TypedDict, cast from typing import Any, TypedDict, cast
import voluptuous as vol import voluptuous as vol
@ -131,18 +132,31 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero
return aio_zc return aio_zc
@callback
def _async_zc_has_functional_dual_stack() -> bool:
"""Return true for platforms that not support IP_ADD_MEMBERSHIP on an AF_INET6 socket.
Zeroconf only supports a single listen socket at this time.
"""
return not sys.platform.startswith("freebsd") and not sys.platform.startswith(
"darwin"
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Zeroconf and make Home Assistant discoverable.""" """Set up Zeroconf and make Home Assistant discoverable."""
zc_args: dict = {} zc_args: dict = {"ip_version": IPVersion.V4Only}
adapters = await network.async_get_adapters(hass) adapters = await network.async_get_adapters(hass)
ipv6 = True
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
ipv6 = False ipv6 = False
zc_args["ip_version"] = IPVersion.V4Only if _async_zc_has_functional_dual_stack():
else: if any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
ipv6 = True
zc_args["ip_version"] = IPVersion.All zc_args["ip_version"] = IPVersion.All
elif not any(adapter["enabled"] and adapter["ipv4"] for adapter in adapters):
zc_args["ip_version"] = IPVersion.V6Only
ipv6 = True
if not ipv6 and network.async_only_default_interface_enabled(adapters): if not ipv6 and network.async_only_default_interface_enabled(adapters):
zc_args["interfaces"] = InterfaceChoice.Default zc_args["interfaces"] = InterfaceChoice.Default
@ -152,6 +166,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
for source_ip in await network.async_get_enabled_source_ips(hass) for source_ip in await network.async_get_enabled_source_ips(hass)
if not source_ip.is_loopback if not source_ip.is_loopback
and not (isinstance(source_ip, IPv6Address) and source_ip.is_global) and not (isinstance(source_ip, IPv6Address) and source_ip.is_global)
and not (
isinstance(source_ip, IPv6Address)
and zc_args["ip_version"] == IPVersion.V4Only
)
and not (
isinstance(source_ip, IPv4Address)
and zc_args["ip_version"] == IPVersion.V6Only
)
] ]
aio_zc = await _async_get_instance(hass, **zc_args) aio_zc = await _async_get_instance(hass, **zc_args)

View File

@ -779,11 +779,13 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
] ]
async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zeroconf): async def test_async_detect_interfaces_setting_empty_route_linux(
"""Test without default interface config and the route returns nothing.""" hass, mock_async_zeroconf
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( ):
hass.config_entries.flow, "async_init" """Test without default interface config and the route returns nothing on linux."""
), patch.object( with patch("homeassistant.components.zeroconf.sys.platform", "linux"), patch(
"homeassistant.components.zeroconf.HaZeroconf"
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch( ), patch(
"homeassistant.components.zeroconf.network.async_get_adapters", "homeassistant.components.zeroconf.network.async_get_adapters",
@ -807,6 +809,33 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero
) )
async def test_async_detect_interfaces_setting_empty_route_freebsd(
hass, mock_async_zeroconf
):
"""Test without default interface config and the route returns nothing on freebsd."""
with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch(
"homeassistant.components.zeroconf.HaZeroconf"
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.network.async_get_adapters",
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
), patch(
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
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=[
"192.168.1.5",
"172.16.1.5",
],
ip_version=IPVersion.V4Only,
)
async def test_get_announced_addresses(hass, mock_async_zeroconf): async def test_get_announced_addresses(hass, mock_async_zeroconf):
"""Test addresses for mDNS announcement.""" """Test addresses for mDNS announcement."""
expected = { expected = {
@ -848,11 +877,13 @@ _ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [
] ]
async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zeroconf): async def test_async_detect_interfaces_explicitly_set_ipv6_linux(
"""Test interfaces are explicitly set when IPv6 is present.""" hass, mock_async_zeroconf
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( ):
hass.config_entries.flow, "async_init" """Test interfaces are explicitly set when IPv6 is present on linux."""
), patch.object( with patch("homeassistant.components.zeroconf.sys.platform", "linux"), patch(
"homeassistant.components.zeroconf.HaZeroconf"
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch( ), patch(
"homeassistant.components.zeroconf.network.async_get_adapters", "homeassistant.components.zeroconf.network.async_get_adapters",
@ -871,6 +902,31 @@ async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zero
) )
async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd(
hass, mock_async_zeroconf
):
"""Test interfaces are explicitly set when IPv6 is present on freebsd."""
with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch(
"homeassistant.components.zeroconf.HaZeroconf"
) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
), patch(
"homeassistant.components.zeroconf.network.async_get_adapters",
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
), patch(
"homeassistant.components.zeroconf.AsyncServiceInfo",
side_effect=get_service_info_mock,
):
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=InterfaceChoice.Default,
ip_version=IPVersion.V4Only,
)
async def test_no_name(hass, mock_async_zeroconf): async def test_no_name(hass, mock_async_zeroconf):
"""Test fallback to Home for mDNS announcement if the name is missing.""" """Test fallback to Home for mDNS announcement if the name is missing."""
hass.config.location_name = "" hass.config.location_name = ""