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 <jan-philipp@bnck.me>

* Process review comments

---------

Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
This commit is contained in:
Franck Nijhof 2025-04-04 22:03:02 +02:00 committed by GitHub
parent 64e1735647
commit 69e241d2e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 1 deletions

View File

@ -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

View File

@ -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."
}
}
}

View File

@ -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': <ANY>,
'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': <IssueSeverity.WARNING: 'warning'>,
'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',
}),
})
# ---

View File

@ -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