From 823f26805459a37c0cbe07617a7f5a56cd5d7330 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Jan 2024 22:58:28 +0100 Subject: [PATCH] Randomize thread network names (#108302) * Randomize thread network names * Use PAN ID as network name suffix * Apply suggestions from code review Co-authored-by: Stefan Agner * Update tests * Format code * Change format of network name again --------- Co-authored-by: Stefan Agner --- homeassistant/components/otbr/config_flow.py | 10 ++++++++-- homeassistant/components/otbr/util.py | 12 ++++++++++++ homeassistant/components/otbr/websocket_api.py | 13 +++++++++++-- tests/components/otbr/test_config_flow.py | 12 +++++++++--- tests/components/otbr/test_websocket_api.py | 9 +++++++-- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index 35772c00a89..b96e276af8b 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -28,7 +28,11 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DEFAULT_CHANNEL, DOMAIN -from .util import get_allowed_channel +from .util import ( + compose_default_network_name, + generate_random_pan_id, + get_allowed_channel, +) _LOGGER = logging.getLogger(__name__) @@ -85,10 +89,12 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.debug( "not importing TLV with channel %s", thread_dataset_channel ) + pan_id = generate_random_pan_id() await api.create_active_dataset( python_otbr_api.ActiveDataSet( channel=allowed_channel if allowed_channel else DEFAULT_CHANNEL, - network_name="home-assistant", + network_name=compose_default_network_name(pan_id), + pan_id=pan_id, ) ) await api.set_enabled(True) diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 85e97209a44..9c47df5eaf7 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -5,6 +5,7 @@ from collections.abc import Callable, Coroutine import dataclasses from functools import wraps import logging +import random from typing import Any, Concatenate, ParamSpec, TypeVar, cast import python_otbr_api @@ -48,6 +49,17 @@ INSECURE_PASSPHRASES = ( ) +def compose_default_network_name(pan_id: int) -> str: + """Generate a default network name.""" + return f"ha-thread-{pan_id:04x}" + + +def generate_random_pan_id() -> int: + """Generate a random PAN ID.""" + # PAN ID is 2 bytes, 0xffff is reserved for broadcast + return random.randint(0, 0xFFFE) + + def _handle_otbr_error( func: Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]], ) -> Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]]: diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py index 0693bc3a325..163152a4bff 100644 --- a/homeassistant/components/otbr/websocket_api.py +++ b/homeassistant/components/otbr/websocket_api.py @@ -16,7 +16,13 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from .const import DEFAULT_CHANNEL, DOMAIN -from .util import OTBRData, get_allowed_channel, update_issues +from .util import ( + OTBRData, + compose_default_network_name, + generate_random_pan_id, + get_allowed_channel, + update_issues, +) @callback @@ -99,10 +105,13 @@ async def websocket_create_network( connection.send_error(msg["id"], "factory_reset_failed", str(exc)) return + pan_id = generate_random_pan_id() try: await data.create_active_dataset( python_otbr_api.ActiveDataSet( - channel=channel, network_name="home-assistant" + channel=channel, + network_name=compose_default_network_name(pan_id), + pan_id=pan_id, ) ) except HomeAssistantError as exc: diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index deb8672b961..1a0216825b4 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -121,9 +121,11 @@ async def test_user_flow_router_not_setup( # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "PUT" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" + pan_id = aioclient_mock.mock_calls[-2][2]["PanId"] assert aioclient_mock.mock_calls[-2][2] == { "Channel": 15, - "NetworkName": "home-assistant", + "NetworkName": f"ha-thread-{pan_id:04x}", + "PanId": pan_id, } assert aioclient_mock.mock_calls[-1][0] == "PUT" @@ -425,9 +427,11 @@ async def test_hassio_discovery_flow_router_not_setup( # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "PUT" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" + pan_id = aioclient_mock.mock_calls[-2][2]["PanId"] assert aioclient_mock.mock_calls[-2][2] == { "Channel": 15, - "NetworkName": "home-assistant", + "NetworkName": f"ha-thread-{pan_id:04x}", + "PanId": pan_id, } assert aioclient_mock.mock_calls[-1][0] == "PUT" @@ -532,9 +536,11 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "PUT" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" + pan_id = aioclient_mock.mock_calls[-2][2]["PanId"] assert aioclient_mock.mock_calls[-2][2] == { "Channel": 15, - "NetworkName": "home-assistant", + "NetworkName": f"ha-thread-{pan_id:04x}", + "PanId": pan_id, } assert aioclient_mock.mock_calls[-1][0] == "PUT" diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index 8288e7e9f70..c9f5327534a 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -105,7 +105,10 @@ async def test_create_network( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 ) as get_active_dataset_tlvs_mock, patch( "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add: + ) as mock_add, patch( + "homeassistant.components.otbr.util.random.randint", + return_value=0x1234, + ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -113,7 +116,9 @@ async def test_create_network( assert msg["result"] is None create_dataset_mock.assert_called_once_with( - python_otbr_api.models.ActiveDataSet(channel=15, network_name="home-assistant") + python_otbr_api.models.ActiveDataSet( + channel=15, network_name="ha-thread-1234", pan_id=0x1234 + ) ) factory_reset_mock.assert_called_once_with() assert len(set_enabled_mock.mock_calls) == 2