1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-07-10 07:13:49 +01:00
Files
supervisor/tests/apps/test_network_isolation.py
Stefan Agner 205b7dd589 Pin stable MAC address for isolated app network endpoints
Docker 28 assigns a random MAC address to macvlan endpoints on every
endpoint creation, while older engines derived it from the endpoint's
IP address. As app containers are recreated on every start, the
isolated endpoint would get a different MAC address on each restart,
making it impossible to reference the app in router or firewall rules.

Pin the MAC address in the endpoint configuration instead, using the
same derivation older engines used (02:42 followed by the static IPv4
octets). With the static IP enforced per app, the MAC address stays
stable across restarts and updates and only changes when the user
assigns a different IP address.

Since the MAC address is now fully determined by the configuration,
report it in the app info API from the configured IP rather than from
the running container's network metadata. This makes it available as
soon as isolation is configured, so users can set up router or
firewall rules before the first start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:54:40 +02:00

87 lines
2.9 KiB
Python

"""Test app network isolation setting."""
from ipaddress import IPv4Address
import pytest
import voluptuous as vol
from supervisor.apps import validate as vd
from supervisor.apps.app import App
from supervisor.apps.utils import rating_security
from supervisor.const import (
ATTR_DRIVER,
ATTR_HOST_NETWORK,
ATTR_INTERFACE,
ATTR_IPV4,
ATTR_NETWORK_ISOLATION,
)
from supervisor.coresys import CoreSys
from supervisor.docker.const import ExternalNetworkDriver, NetworkIsolationConfig
from ..const import TEST_INTERFACE_ETH_NAME
TEST_CONFIG = NetworkIsolationConfig(
driver=ExternalNetworkDriver.MACVLAN,
interface=TEST_INTERFACE_ETH_NAME,
ipv4=IPv4Address("192.168.2.50"),
)
def test_schema_defaults_to_macvlan():
"""Test network isolation schema applies macvlan driver default."""
data = vd.SCHEMA_NETWORK_ISOLATION(
{ATTR_INTERFACE: TEST_INTERFACE_ETH_NAME, ATTR_IPV4: "192.168.2.50"}
)
assert data[ATTR_DRIVER] == ExternalNetworkDriver.MACVLAN
assert data[ATTR_IPV4] == "192.168.2.50"
@pytest.mark.parametrize(
"config",
[
{ATTR_INTERFACE: TEST_INTERFACE_ETH_NAME, ATTR_IPV4: "not-an-ip"},
{ATTR_INTERFACE: TEST_INTERFACE_ETH_NAME, ATTR_IPV4: "fd00::1"},
{ATTR_INTERFACE: TEST_INTERFACE_ETH_NAME},
{ATTR_IPV4: "192.168.2.50"},
{
ATTR_INTERFACE: TEST_INTERFACE_ETH_NAME,
ATTR_IPV4: "192.168.2.50",
ATTR_DRIVER: "bridge",
},
],
)
def test_schema_invalid(config: dict[str, str]):
"""Test invalid network isolation configurations are rejected."""
with pytest.raises(vol.Invalid):
vd.SCHEMA_NETWORK_ISOLATION(config)
async def test_network_isolation_persistence(coresys: CoreSys, install_app_ssh: App):
"""Test network isolation setting round-trips through persisted data."""
assert install_app_ssh.network_isolation is None
assert install_app_ssh.external_mac_address is None
install_app_ssh.network_isolation = TEST_CONFIG
assert install_app_ssh.persist[ATTR_NETWORK_ISOLATION] == {
ATTR_DRIVER: "macvlan",
ATTR_INTERFACE: TEST_INTERFACE_ETH_NAME,
ATTR_IPV4: "192.168.2.50",
}
assert install_app_ssh.network_isolation == TEST_CONFIG
# MAC is derived from the static IP, known without a running container
assert install_app_ssh.external_mac_address == "02:42:c0:a8:02:32"
install_app_ssh.network_isolation = None
assert install_app_ssh.network_isolation is None
assert ATTR_NETWORK_ISOLATION not in install_app_ssh.persist
async def test_rating_security_with_isolation(coresys: CoreSys, install_app_ssh: App):
"""Test host network rating penalty is not applied with isolation."""
install_app_ssh.data[ATTR_HOST_NETWORK] = True
host_network_rating = rating_security(install_app_ssh)
install_app_ssh.network_isolation = TEST_CONFIG
assert rating_security(install_app_ssh) == host_network_rating + 1