"""Test the Network Configuration.""" from unittest.mock import Mock, patch import ifaddr from homeassistant.components import network from homeassistant.components.network.const import ( ATTR_ADAPTERS, ATTR_CONFIGURED_ADAPTERS, MDNS_TARGET_IP, STORAGE_KEY, STORAGE_VERSION, ) from homeassistant.setup import async_setup_component _NO_LOOPBACK_IPADDR = "192.168.1.5" _LOOPBACK_IPADDR = "127.0.0.1" def _generate_mock_adapters(): mock_lo0 = Mock(spec=ifaddr.Adapter) mock_lo0.nice_name = "lo0" mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] mock_eth0 = Mock(spec=ifaddr.Adapter) mock_eth0.nice_name = "eth0" mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] mock_eth1 = Mock(spec=ifaddr.Adapter) mock_eth1.nice_name = "eth1" mock_eth1.ips = [ifaddr.IP("192.168.1.5", 23, "eth1")] mock_vtun0 = Mock(spec=ifaddr.Adapter) mock_vtun0.nice_name = "vtun0" mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")] return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_storage): """Test without default interface config and the route returns a non-loopback address.""" with patch( "homeassistant.components.network.util.socket.socket.getsockname", return_value=[_NO_LOOPBACK_IPADDR], ), patch( "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) await hass.async_block_till_done() network_obj = hass.data[network.DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { "auto": False, "default": False, "enabled": False, "ipv4": [], "ipv6": [ { "address": "2001:db8::", "network_prefix": 8, "flowinfo": 1, "scope_id": 1, } ], "name": "eth0", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], "ipv6": [], "name": "lo0", }, { "auto": True, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], "ipv6": [], "name": "eth1", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], "ipv6": [], "name": "vtun0", }, ] async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage): """Test without default interface config and the route returns a loopback address.""" with patch( "homeassistant.components.network.util.socket.socket.getsockname", return_value=[_LOOPBACK_IPADDR], ), patch( "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) await hass.async_block_till_done() network_obj = hass.data[network.DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { "auto": True, "default": False, "enabled": True, "ipv4": [], "ipv6": [ { "address": "2001:db8::", "network_prefix": 8, "flowinfo": 1, "scope_id": 1, } ], "name": "eth0", }, { "auto": False, "default": True, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], "ipv6": [], "name": "lo0", }, { "auto": True, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], "ipv6": [], "name": "eth1", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], "ipv6": [], "name": "vtun0", }, ] async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): """Test without default interface config and the route returns nothing.""" with patch( "homeassistant.components.network.util.socket.socket.getsockname", return_value=[], ), patch( "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) await hass.async_block_till_done() network_obj = hass.data[network.DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { "auto": True, "default": False, "enabled": True, "ipv4": [], "ipv6": [ { "address": "2001:db8::", "network_prefix": 8, "flowinfo": 1, "scope_id": 1, } ], "name": "eth0", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], "ipv6": [], "name": "lo0", }, { "auto": True, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], "ipv6": [], "name": "eth1", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], "ipv6": [], "name": "vtun0", }, ] async def test_async_detect_interfaces_setting_exception(hass, hass_storage): """Test without default interface config and the route throws an exception.""" with patch( "homeassistant.components.network.util.socket.socket.getsockname", side_effect=AttributeError, ), patch( "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) await hass.async_block_till_done() network_obj = hass.data[network.DOMAIN] assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { "auto": True, "default": False, "enabled": True, "ipv4": [], "ipv6": [ { "address": "2001:db8::", "network_prefix": 8, "flowinfo": 1, "scope_id": 1, } ], "name": "eth0", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], "ipv6": [], "name": "lo0", }, { "auto": True, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], "ipv6": [], "name": "eth1", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], "ipv6": [], "name": "vtun0", }, ] async def test_interfaces_configured_from_storage(hass, hass_storage): """Test settings from storage are preferred over auto configure.""" hass_storage[STORAGE_KEY] = { "version": STORAGE_VERSION, "key": STORAGE_KEY, "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, } with patch( "homeassistant.components.network.util.socket.socket.getsockname", return_value=[_NO_LOOPBACK_IPADDR], ), patch( "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) await hass.async_block_till_done() network_obj = hass.data[network.DOMAIN] assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] assert network_obj.adapters == [ { "auto": False, "default": False, "enabled": True, "ipv4": [], "ipv6": [ { "address": "2001:db8::", "network_prefix": 8, "flowinfo": 1, "scope_id": 1, } ], "name": "eth0", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], "ipv6": [], "name": "lo0", }, { "auto": True, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], "ipv6": [], "name": "eth1", }, { "auto": False, "default": False, "enabled": True, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], "ipv6": [], "name": "vtun0", }, ] async def test_interfaces_configured_from_storage_websocket_update( hass, hass_ws_client, hass_storage ): """Test settings from storage can be updated via websocket api.""" hass_storage[STORAGE_KEY] = { "version": STORAGE_VERSION, "key": STORAGE_KEY, "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, } with patch( "homeassistant.components.network.util.socket.socket.getsockname", return_value=[_NO_LOOPBACK_IPADDR], ), patch( "homeassistant.components.network.util.ifaddr.get_adapters", return_value=_generate_mock_adapters(), ): assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) await hass.async_block_till_done() network_obj = hass.data[network.DOMAIN] assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] ws_client = await hass_ws_client(hass) await ws_client.send_json({"id": 1, "type": "network"}) response = await ws_client.receive_json() assert response["success"] assert response["result"][ATTR_CONFIGURED_ADAPTERS] == ["eth0", "eth1", "vtun0"] assert response["result"][ATTR_ADAPTERS] == [ { "auto": False, "default": False, "enabled": True, "ipv4": [], "ipv6": [ { "address": "2001:db8::", "network_prefix": 8, "flowinfo": 1, "scope_id": 1, } ], "name": "eth0", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], "ipv6": [], "name": "lo0", }, { "auto": True, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], "ipv6": [], "name": "eth1", }, { "auto": False, "default": False, "enabled": True, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], "ipv6": [], "name": "vtun0", }, ] await ws_client.send_json( {"id": 2, "type": "network/configure", "config": {ATTR_CONFIGURED_ADAPTERS: []}} ) response = await ws_client.receive_json() assert response["result"][ATTR_CONFIGURED_ADAPTERS] == [] await ws_client.send_json({"id": 3, "type": "network"}) response = await ws_client.receive_json() assert response["result"][ATTR_CONFIGURED_ADAPTERS] == [] assert response["result"][ATTR_ADAPTERS] == [ { "auto": False, "default": False, "enabled": False, "ipv4": [], "ipv6": [ { "address": "2001:db8::", "network_prefix": 8, "flowinfo": 1, "scope_id": 1, } ], "name": "eth0", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], "ipv6": [], "name": "lo0", }, { "auto": True, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], "ipv6": [], "name": "eth1", }, { "auto": False, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], "ipv6": [], "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"