From 4b1bd358812d4833ff50f307da7cc221f48c81d5 Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Wed, 21 Dec 2022 14:52:40 -0500 Subject: [PATCH 1/2] fix: bind to a specific local IPv6 address during pair verify This will prevent future traffic from picking the "best" address to communicate with an accessory. Current products tie sessions to the original IPv6 address and return a CoAP error code 4.04 when a new address attempts to continue the session. It is likely that this could explode in fun, new ways: the DHCPv6 server returning a new address, the host selecting a new random SLAAC address, etc. --- aiohomekit/controller/coap/connection.py | 20 +++++++++++++++++++- aiohomekit/controller/coap/pairing.py | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/aiohomekit/controller/coap/connection.py b/aiohomekit/controller/coap/connection.py index d1b58f54f..26fb2f989 100644 --- a/aiohomekit/controller/coap/connection.py +++ b/aiohomekit/controller/coap/connection.py @@ -19,6 +19,7 @@ import asyncio import logging import random +import socket import struct from typing import Any import uuid @@ -244,8 +245,10 @@ def __init__(self, owner, host, port): self.address = f"[{host}]:{port}" self.connection_lock = asyncio.Lock() self.enc_ctx = None + self.host = host self.owner = owner self.pair_setup_client = None + self.port = port async def reconnect_soon(self): if not self.enc_ctx: @@ -322,14 +325,29 @@ async def do_pair_setup_finish(self, pin, salt, srpB): return pairing + def _get_local_ipv6_addr(self, remote): + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s.connect(remote) + addr = s.getsockname()[0] + s.close() + return addr + async def do_pair_verify(self, pairing_data): if self.is_connected: logger.warning("Connecting to connected device?") await self.enc_ctx.coap_ctx.shutdown() self.enc_ctx = None + try: + local_address = self._get_local_ipv6_addr((self.host, self.port)) + except Exception as e: + logger.warning( + f"Unable to get local IPv6 address to connect to {self.address}: {e}" + ) + raise AccessoryDisconnectedError("Unable to get local IPv6 address") from e + root = resource.Site() - coap_client = await Context.create_server_context(root, bind=("::", 0)) + coap_client = await Context.create_server_context(root, bind=(local_address, 0)) uri = "coap://%s/2" % (self.address) logger.debug(f"Pair verify uri={uri}") diff --git a/aiohomekit/controller/coap/pairing.py b/aiohomekit/controller/coap/pairing.py index 017587ba4..fdd36f7e7 100644 --- a/aiohomekit/controller/coap/pairing.py +++ b/aiohomekit/controller/coap/pairing.py @@ -53,6 +53,8 @@ def _async_endpoint_changed(self) -> None: self.connection.address = ( f"[{self.description.address}]:{self.description.port}" ) + self.connection.host = self.description.address + self.connection.port = self.description.port async_create_task(self.connection.reconnect_soon()) @property From 690193952a14355926c6f6abf0bb8e80a38a33ec Mon Sep 17 00:00:00 2001 From: Josh Roys Date: Wed, 21 Dec 2022 22:38:10 -0500 Subject: [PATCH 2/2] fix(coap): handle accessories dropping sessions A CoAP error code of 4.04 can indicate that the accessory has no record of the HAP session. This will certainly be the case if a device has lost power. Shutdown the session on our side so we can trigger a reconnect. --- aiohomekit/controller/coap/connection.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aiohomekit/controller/coap/connection.py b/aiohomekit/controller/coap/connection.py index 26fb2f989..3ea94cc59 100644 --- a/aiohomekit/controller/coap/connection.py +++ b/aiohomekit/controller/coap/connection.py @@ -171,7 +171,13 @@ async def post_bytes(self, payload: bytes, timeout: int = 16.0): except (NetworkError, asyncio.TimeoutError): raise AccessoryDisconnectedError("Request timeout") - if response.code != Code.CHANGED: + if response.code == Code.NOT_FOUND: + # maybe the accessory lost power or was otherwise rebooted + logger.error("CoAP POST returned 404, our session is gone.") + await self.coap_ctx.shutdown() + self.coap_ctx = None + raise AccessoryDisconnectedError("Accessory lost our session") + elif response.code != Code.CHANGED: logger.warning(f"CoAP POST returned unexpected code {response}") return await self._decrypt_response(response)