diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 3f19103acaa..6f11b0947d8 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from . import util from .const import ( ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, @@ -31,6 +32,19 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]: return network.adapters +@bind_hass +async def async_get_source_ip(hass: HomeAssistant, target_ip: str) -> str | None: + """Get the source ip for a target ip.""" + adapters = await async_get_adapters(hass) + all_ipv4s = [] + for adapter in adapters: + if adapter["enabled"] and (ipv4s := adapter["ipv4"]): + all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s]) + + source_ip = util.async_get_source_ip(target_ip) + return source_ip if source_ip in all_ipv4s else all_ipv4s[0] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up network for Home Assistant.""" diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index 41d87d5a805..bc4c543842f 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -7,6 +7,7 @@ from homeassistant.components import network from homeassistant.components.network.const import ( ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, + MDNS_TARGET_IP, STORAGE_KEY, STORAGE_VERSION, ) @@ -444,3 +445,66 @@ async def test_interfaces_configured_from_storage_websocket_update( "name": "vtun0", }, ] + + +async def test_async_get_source_ip_matching_interface(hass, hass_storage): + """Test getting the source ip address with interface matching.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=["192.168.1.5"], + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" + + +async def test_async_get_source_ip_interface_not_match(hass, hass_storage): + """Test getting the source ip address with interface does not match.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["vtun0"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=["192.168.1.5"], + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "169.254.3.2" + + +async def test_async_get_source_ip_cannot_determine_target(hass, hass_storage): + """Test getting the source ip address when getsockname fails.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, + } + + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[None], + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5"