Skip to content

Commit

Permalink
Scope link-local IPv6 addresses received from WebSocket (#501)
Browse files Browse the repository at this point in the history
  • Loading branch information
agners authored Jan 11, 2024
1 parent 6bbca7b commit 234b80d
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 7 deletions.
12 changes: 11 additions & 1 deletion matter_server/server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
default=None,
help="Log file to write to (optional).",
)
parser.add_argument(
"--primary-interface",
type=str,
default=None,
help="Primary network interface for link-local addresses (optional).",
)

args = parser.parse_args()

Expand Down Expand Up @@ -85,7 +91,11 @@ def main() -> None:

# Init server
server = MatterServer(
args.storage_path, int(args.vendorid), int(args.fabricid), int(args.port)
args.storage_path,
int(args.vendorid),
int(args.fabricid),
int(args.port),
args.primary_interface,
)

async def handle_stop(loop: asyncio.AbstractEventLoop) -> None:
Expand Down
6 changes: 4 additions & 2 deletions matter_server/server/device_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ async def commission_on_network(
raise RuntimeError("Device Controller not initialized.")

node_id = self._get_next_node_id()
if ip_addr is not None:
ip_addr = self.server.scope_ipv6_lla(ip_addr)

attempts = 0
# we retry commissioning a few times as we've seen devices in the wild
Expand All @@ -281,10 +283,10 @@ async def commission_on_network(
filter=filter,
)
else:
# Ip-adress provided, use CommissionIP method
LOGGER.info(
"Starting Matter commissioning with IP using Node ID %s (attempt %s/%s).",
"Starting Matter commissioning using Node ID %s and IP %s (attempt %s/%s).",
node_id,
ip_addr,
attempts,
MAX_COMMISSION_RETRIES,
)
Expand Down
33 changes: 32 additions & 1 deletion matter_server/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from __future__ import annotations

import asyncio
import ipaddress
import logging
from typing import Any, Callable, Set
from typing import Any, Callable, Set, cast
import weakref

from aiohttp import web
Expand Down Expand Up @@ -61,12 +62,14 @@ def __init__(
vendor_id: int,
fabric_id: int,
port: int,
primary_interface: str | None,
) -> None:
"""Initialize the Matter Server."""
self.storage_path = storage_path
self.vendor_id = vendor_id
self.fabric_id = fabric_id
self.port = port
self.primary_interface = primary_interface
self.logger = logging.getLogger(__name__)
self.app = web.Application()
self.loop: asyncio.AbstractEventLoop | None = None
Expand Down Expand Up @@ -166,6 +169,34 @@ def signal_event(self, evt: EventType, data: Any = None) -> None:
else:
callback(evt, data)

def scope_ipv6_lla(self, ip_addr: str) -> str:
"""Scope IPv6 link-local addresses to primary interface.
IPv6 link-local addresses received through the websocket might have no
scope_id or a scope_id which isn't valid on this device. Just assume the
device is connected on the primary interface.
"""
ip_addr_parsed = ipaddress.ip_address(ip_addr)
if not ip_addr_parsed.is_link_local or ip_addr_parsed.version != 6:
return ip_addr

ip_addr_parsed = cast(ipaddress.IPv6Address, ip_addr_parsed)

if ip_addr_parsed.scope_id is not None:
# This type of IPv6 manipulation is not supported by the ipaddress lib
ip_addr = ip_addr.split("%")[0]

# Rely on host OS routing table
if self.primary_interface is None:
return ip_addr

self.logger.debug(
"Setting scope of link-local IP address %s to %s",
ip_addr,
self.primary_interface,
)
return ip_addr + "%" + self.primary_interface

def register_api_command(
self,
command: str,
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ dependencies = [
"coloredlogs",
"dacite",
"orjson",
"home-assistant-chip-clusters==2023.12.0"
"home-assistant-chip-clusters==2024.1.0"
]

[project.optional-dependencies]
server = [
"home-assistant-chip-core==2023.12.0",
"home-assistant-chip-core==2024.1.0",
"cryptography==41.0.7"
]
test = [
Expand Down
2 changes: 1 addition & 1 deletion tests/server/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def fetch_certificates_fixture() -> Generator[MagicMock, None, None]:
@pytest.fixture(name="server")
async def server_fixture() -> AsyncGenerator[MatterServer, None]:
"""Yield a server."""
server = MatterServer("test_storage_path", 1234, 5678, 5580)
server = MatterServer("test_storage_path", 1234, 5678, 5580, None)
await server.start()
yield server
await server.stop()
Expand Down

0 comments on commit 234b80d

Please sign in to comment.