From 69e241d2e652b6e15ba1cc4fe9dca878c4d7a61d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Apr 2025 22:03:02 +0200 Subject: [PATCH] Add Docker host networking issue detection (#142259) * Add Docker host networking issue detection * Update homeassistant/components/network/strings.json Co-authored-by: Jan-Philipp Benecke * Process review comments --------- Co-authored-by: Jan-Philipp Benecke --- homeassistant/components/network/__init__.py | 31 +++++++++++- homeassistant/components/network/strings.json | 6 +++ .../network/snapshots/test_init.ambr | 22 +++++++++ tests/components/network/test_init.py | 49 +++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/components/network/snapshots/test_init.ambr diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 200cce86997..14c7dc55cf0 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -4,12 +4,14 @@ from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_interface import logging +from pathlib import Path from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from homeassistant.loader import bind_hass +from homeassistant.util import package from . import util from .const import ( @@ -27,6 +29,19 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +def _check_docker_without_host_networking() -> bool: + """Check if we are not using host networking in Docker.""" + if not package.is_docker_env(): + # We are not in Docker, so we don't need to check for host networking + return True + + if Path("/proc/sys/net/ipv4/ip_forward").exists(): + # If we can read this file, we likely have host networking + return True + + return False + + @bind_hass async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: """Get the network adapter configuration.""" @@ -166,5 +181,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_get_network(hass) + if not await hass.async_add_executor_job(_check_docker_without_host_networking): + docs_url = "https://docs.docker.com/network/network-tutorial-host/" + install_url = "https://www.home-assistant.io/installation/linux#install-home-assistant-container" + ir.async_create_issue( + hass, + DOMAIN, + "docker_host_network", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="docker_host_network", + learn_more_url=install_url, + translation_placeholders={"docs_url": docs_url, "install_url": install_url}, + ) + async_register_websocket_commands(hass) return True diff --git a/homeassistant/components/network/strings.json b/homeassistant/components/network/strings.json index 6aca7343221..3e135fff60b 100644 --- a/homeassistant/components/network/strings.json +++ b/homeassistant/components/network/strings.json @@ -6,5 +6,11 @@ "ipv6_addresses": "IPv6 addresses", "announce_addresses": "Announce addresses" } + }, + "issues": { + "docker_host_network": { + "title": "Home Assistant is not using host networking", + "description": "Home Assistant is running in a container without host networking mode. This can cause networking issues with device discovery, multicast, broadcast, other network features, and incorrectly detecting its own URL and IP addresses, causing issues with media players and sending audio responses to voice assistants.\n\nIt is recommended to run Home Assistant with host networking by adding the `--network host` flag to your Docker run command or setting `network_mode: host` in your `docker-compose.yml` file.\n\nSee the [Docker documentation]({docs_url}) for more information about Docker host networking and refer to the [Home Assistant installation guide]({install_url}) for our recommended and supported setup." + } } } diff --git a/tests/components/network/snapshots/test_init.ambr b/tests/components/network/snapshots/test_init.ambr new file mode 100644 index 00000000000..268c8e0d44f --- /dev/null +++ b/tests/components/network/snapshots/test_init.ambr @@ -0,0 +1,22 @@ +# serializer version: 1 +# name: test_repair_docker_host_network_without_host_networking[mock_socket0] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': None, + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'network', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'docker_host_network', + 'learn_more_url': 'https://www.home-assistant.io/installation/linux#install-home-assistant-container', + 'severity': , + 'translation_key': 'docker_host_network', + 'translation_placeholders': dict({ + 'docs_url': 'https://docs.docker.com/network/network-tutorial-host/', + 'install_url': 'https://www.home-assistant.io/installation/linux#install-home-assistant-container', + }), + }) +# --- diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index a2352e6af9e..372dba1772d 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -1,10 +1,13 @@ """Test the Network Configuration.""" +from __future__ import annotations + from ipaddress import IPv4Address from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import network from homeassistant.components.network.const import ( @@ -17,6 +20,7 @@ from homeassistant.components.network.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from . import LOOPBACK_IPADDR, NO_LOOPBACK_IPADDR @@ -801,3 +805,48 @@ async def test_websocket_network_url( "external": None, "cloud": None, } + + +@pytest.mark.parametrize("mock_socket", [[]], indirect=True) +@pytest.mark.usefixtures("mock_socket") +async def test_repair_docker_host_network_not_docker( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test repair is not created when not in Docker.""" + with patch("homeassistant.util.package.is_docker_env", return_value=False): + assert await async_setup_component(hass, "network", {}) + + assert not issue_registry.async_get_issue(DOMAIN, "docker_host_network") + + +@pytest.mark.parametrize("mock_socket", [[]], indirect=True) +@pytest.mark.usefixtures("mock_socket") +async def test_repair_docker_host_network_with_host_networking( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test repair is not created when in Docker with host networking.""" + with ( + patch("homeassistant.util.package.is_docker_env", return_value=True), + patch("homeassistant.components.network.Path.exists", return_value=True), + ): + assert await async_setup_component(hass, "network", {}) + + assert not issue_registry.async_get_issue(DOMAIN, "docker_host_network") + + +@pytest.mark.parametrize("mock_socket", [[]], indirect=True) +@pytest.mark.usefixtures("mock_socket") +async def test_repair_docker_host_network_without_host_networking( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test repair is created when in Docker without host networking.""" + with ( + patch("homeassistant.util.package.is_docker_env", return_value=True), + patch("homeassistant.components.network.Path.exists", return_value=False), + ): + assert await async_setup_component(hass, "network", {}) + + assert (issue := issue_registry.async_get_issue(DOMAIN, "docker_host_network")) + assert issue == snapshot