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 contextlib import suppress
import fnmatch
from ipaddress import IPv6Address, ip_address
from ipaddress import IPv4Address, IPv6Address, ip_address
import logging
import socket
import sys
from typing import Any, TypedDict, cast
import voluptuous as vol
@ -131,18 +132,31 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero
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:
"""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)
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
ipv6 = False
if _async_zc_has_functional_dual_stack():
if any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
ipv6 = True
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):
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)
if not source_ip.is_loopback
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)

View file

@ -779,11 +779,13 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [
]
async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zeroconf):
"""Test without default interface config and the route returns nothing."""
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
hass.config_entries.flow, "async_init"
), patch.object(
async def test_async_detect_interfaces_setting_empty_route_linux(
hass, mock_async_zeroconf
):
"""Test without default interface config and the route returns nothing on linux."""
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
), patch(
"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):
"""Test addresses for mDNS announcement."""
expected = {
@ -848,11 +877,13 @@ _ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [
]
async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zeroconf):
"""Test interfaces are explicitly set when IPv6 is present."""
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
hass.config_entries.flow, "async_init"
), patch.object(
async def test_async_detect_interfaces_explicitly_set_ipv6_linux(
hass, mock_async_zeroconf
):
"""Test interfaces are explicitly set when IPv6 is present on linux."""
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
), patch(
"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):
"""Test fallback to Home for mDNS announcement if the name is missing."""
hass.config.location_name = ""