Repository: XKNX/xknx (python)

Issue:
OverflowErrors on secure timer
**Description of problem:**
I am trying to set up Home-Assistant to KNX using Routing with Data Secure and IP Secure using an MDT Router. I run into troubles which I tried to debug a bit and appear to be also related to the router. When sending data to the bus I get `OverflowError`s when converting the timestamp. This is true for every telegram, below the debug output of one when I trigger a switch.

The source of the problem appears to be the secure timer value. These show up regularly from the router (which I guess is expected), however the `timer_value` is already at the maximum a 6 byte unsigned int can hold (debug output below). This also never changes. I have no clue how the timer value is generated, but this appears wrong.

If there's a way xknx can influence the timer value it would be great. Either way xknx should handle an overflow on the timer value more gracefully / a better error message).

- [ ] using xknx standalone
- [x] using Home-Assistant knx integration

**Version information:**
- xknx / Home-Assistant release with the issue: 2024.02.2
- last working xknx / Home-Assistant release (if known): none

**KNX installation:**
* MDT SCN-IP100.03 IP Router (Firmware 3.0.6)

**Problem-relevant `configuration.yaml` entries (fill out even if it seems unimportant):**

**Diagnostic data of the config entry (only when Home Assistant is used)**

**Traceback (if applicable):**
Overflow:
```
2024-02-19 08:11:38.214 DEBUG (MainThread) [xknx.telegram] <Telegram direction="Outgoing" source_address="0.0.240" destination_address="4/2/0" payload="<GroupValueWrite value="<DPTBinary value="True" />" />" />
2024-02-19 08:11:38.215 DEBUG (MainThread) [xknx.cemi] Outgoing CEMI: <CEMIFrame code="L_DATA_REQ" info="CEMIInfo("")" data="CEMILData(src_addr="IndividualAddress("0.0.240")" dst_addr="GroupAddress("4/2/0")" flags="1011110011100000" tpci="TDataGroup()" payload="<GroupValueWrite value="<DPTBinary value="True" />" />")" />
2024-02-19 08:11:38.216 DEBUG (KNX Interface) [xknx.knx] Encrypting frame: <KNXIPFrame <KNXIPHeader HeaderLength="6" ProtocolVersion="16" KNXIPServiceType="ROUTING_INDICATION" TotalLength="17" /> body="<RoutingIndication cemi="2900bce000f02200010081" />" />
2024-02-19 08:11:38.218 ERROR (MainThread) [xknx.log] Unexpected error while processing outgoing telegram <Telegram direction="Outgoing" source_address="0.0.240" destination_address="4/2/0" payload="<GroupValueWrite value="<DPTBinary value="True" />" />" />
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/xknx/core/telegram_queue.py", line 169, in _outgoing_rate_limiter
    await self.process_telegram_outgoing(telegram)
  File "/usr/local/lib/python3.12/site-packages/xknx/core/telegram_queue.py", line 205, in process_telegram_outgoing
    await self.xknx.cemi_handler.send_telegram(telegram)
  File "/usr/local/lib/python3.12/site-packages/xknx/cemi/cemi_handler.py", line 74, in send_telegram
    await self.xknx.knxip_interface.send_cemi(cemi)
  File "/usr/local/lib/python3.12/site-packages/xknx/io/knxip_interface.py", line 560, in send_cemi
    return await self._await_from_connection_thread(self._interface.send_cemi(cemi))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/xknx/io/knxip_interface.py", line 533, in _await_from_connection_thread
    return fut.result()
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/concurrent/futures/_base.py", line 449, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/usr/local/lib/python3.12/site-packages/xknx/io/routing.py", line 226, in send_cemi
    self._send_knxipframe(KNXIPFrame.init_from_body(routing_indication))
  File "/usr/local/lib/python3.12/site-packages/xknx/io/routing.py", line 233, in _send_knxipframe
    self.transport.send(knxipframe)
  File "/usr/local/lib/python3.12/site-packages/xknx/io/ip_secure.py", line 468, in send
    knxipframe = self.encrypt_frame(plain_frame=knxipframe)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/xknx/io/ip_secure.py", line 114, in encrypt_frame
    sequence_information = self.get_sequence_information()
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/xknx/io/ip_secure.py", line 475, in get_sequence_information
    return self.secure_timer.get_for_outgoing_secure_wrapper().to_bytes(6, "big")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OverflowError: int too big to convert
```

Timestamp received:
```
2024-02-19 08:11:42.516 DEBUG (KNX Interface) [xknx.raw_socket] Received from ('192.168.0.58', 3671): 061009550024ffffffffffff008381401f8ab08570472a83eb3e2f471ee362803b1f3371
2024-02-19 08:11:42.516 DEBUG (KNX Interface) [xknx.knx] Received from 192.168.0.58:3671: <KNXIPFrame <KNXIPHeader HeaderLength="6" ProtocolVersion="16" KNXIPServiceType="TIMER_NOTIFY" TotalLength="36" /> body="<TimerNotify timer_value="281474976710655" serial_number="008381401f8a" message_tag="b085" message_authentication_code="70472a83eb3e2f471ee362803b1f3371" />" />
```


Pull request description:
Better error message for unlikely IP Secure errors
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->


Fixes #1411

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] Code quality improvements to existing code or addition of tests

## Checklist

- [ ] The documentation has been adjusted accordingly
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog (docs/changelog.md)

Patch:
diff --git a/xknx/exceptions/__init__.py b/xknx/exceptions/__init__.py
index 21de11ce..ef005f5b 100644
--- a/xknx/exceptions/__init__.py
+++ b/xknx/exceptions/__init__.py
@@ -12,11 +12,11 @@ from .exception import (
     DeviceIllegalValue,
     IncompleteKNXIPFrame,
     InvalidSecureConfiguration,
+    IPSecureError,
     KNXSecureValidationError,
     ManagementConnectionError,
     ManagementConnectionRefused,
     ManagementConnectionTimeout,
-    SecureException,
     TunnellingAckError,
     UnsupportedCEMIMessage,
     XKNXException,
@@ -38,7 +38,7 @@ __all__ = [
     "IncompleteKNXIPFrame",
     "InvalidSecureConfiguration",
     "KNXSecureValidationError",
-    "SecureException",
+    "IPSecureError",
     "TunnellingAckError",
     "UnsupportedCEMIMessage",
     "XKNXException",
diff --git a/xknx/exceptions/exception.py b/xknx/exceptions/exception.py
index 91870d29..c8efb9dd 100644
--- a/xknx/exceptions/exception.py
+++ b/xknx/exceptions/exception.py
@@ -40,6 +40,10 @@ class TunnellingAckError(CommunicationError):
     """No ACK or error status received from UDP KNX server for sent Telegram."""
 
 
+class IPSecureError(CommunicationError):
+    """Error in IP Secure communication."""
+
+
 class CouldNotParseTelegram(XKNXException):
     """Could not parse telegram error."""
 
@@ -179,11 +183,7 @@ class DataSecureError(XKNXException):
         self.log_level = log_level
 
 
-class SecureException(XKNXException):
-    """Exception class for IP secure handling."""
-
-
-class InvalidSecureConfiguration(SecureException):
+class InvalidSecureConfiguration(XKNXException):
     """Exception class used when the secure configuration is invalid."""
 
 
diff --git a/xknx/io/ip_secure.py b/xknx/io/ip_secure.py
index b7c52d42..724321e1 100644
--- a/xknx/io/ip_secure.py
+++ b/xknx/io/ip_secure.py
@@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric.x25519 import (
 from xknx.exceptions import (
     CommunicationError,
     CouldNotParseKNXIP,
+    IPSecureError,
     KNXSecureValidationError,
 )
 from xknx.knxip import (
@@ -113,7 +114,16 @@ class _IPSecureTransportLayer(ABC):
 
     def encrypt_frame(self, plain_frame: KNXIPFrame) -> KNXIPFrame:
         """Wrap KNX/IP frame in SecureWrapper."""
-        sequence_information = self.get_sequence_information()
+        try:
+            sequence_information = self.get_sequence_information()
+        except OverflowError as err:
+            raise IPSecureError(
+                "KNX IP Secure sequence counter overflow."
+                "\nCongratulations! You've managed to overflow a counter designed to last about 9000 years! "
+                "Before celebrating, please check for any malfunctioning devices or suspicious activity "
+                "in your installation. Once you've ensured everything's safe, reset the secure session "
+                "to restore normal operation."
+            ) from err
         message_tag = self.get_message_tag()
         plain_payload = plain_frame.to_knx()  # P
         payload_length = len(plain_payload)  # Q
@@ -222,7 +232,7 @@ class SecureSession(TCPTransport, _IPSecureTransportLayer):
             request_authentication.response.status
             != SecureSessionStatusCode.STATUS_AUTHENTICATION_SUCCESS
         ):
-            raise CommunicationError(
+            raise IPSecureError(
                 f"Secure session authentication failed: {request_authentication.response.status}"
             )
         self._session_status_handler = self.register_callback(
@@ -259,7 +269,7 @@ class SecureSession(TCPTransport, _IPSecureTransportLayer):
                 mac=session_response.message_authentication_code,
             )
             if mac_tr != response_mac_cbc:
-                raise CommunicationError("SessionResponse MAC verification failed.")
+                raise IPSecureError("SessionResponse MAC verification failed.")
         # calculate session key
         ecdh_shared_secret = self._private_key.exchange(self._peer_public_key)
         self._key = sha256_hash(ecdh_shared_secret)[:16]


Test patch:
diff --git a/test/io_tests/secure_group_test.py b/test/io_tests/secure_group_test.py
index 0cf4c41f..11c7dfbc 100644
--- a/test/io_tests/secure_group_test.py
+++ b/test/io_tests/secure_group_test.py
@@ -3,7 +3,10 @@
 import asyncio
 from unittest.mock import Mock, patch
 
+import pytest
+
 from xknx.cemi import CEMIFrame, CEMILData, CEMIMessageCode
+from xknx.exceptions import IPSecureError
 from xknx.io.const import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT, XKNX_SERIAL_NUMBER
 from xknx.io.ip_secure import SecureGroup
 from xknx.knxip import HPAI, KNXIPFrame, RoutingIndication, SecureWrapper, TimerNotify
@@ -308,7 +311,7 @@ class TestSecureGroup:
         mock_super_connect,
         time_travel,
     ):
-        """Test handling of received TimerNotify frames."""
+        """Test handling of received SecureWrapper frames."""
         mock_monotonic_ms.return_value = 0
         secure_group = SecureGroup(
             local_addr=self.mock_addr,
@@ -423,17 +426,15 @@ class TestSecureGroup:
 
     @patch("xknx.io.ip_secure.SecureSequenceTimer._notify_timer_expired")
     @patch("xknx.io.ip_secure.SecureSequenceTimer._monotonic_ms")
-    @patch("xknx.io.transport.udp_transport.UDPTransport.handle_knxipframe")
     async def test_send_secure_wrapper(
         self,
-        mock_super_handle_knxipframe,
         mock_monotonic_ms,
         _mock_notify_timer_expired,  # we don't want to actually send here
         mock_super_send,
         mock_super_connect,
         time_travel,
     ):
-        """Test handling of received TimerNotify frames."""
+        """Test sending SecureWrapper frames."""
         mock_monotonic_ms.return_value = 0
         secure_group = SecureGroup(
             local_addr=self.mock_addr,
@@ -472,6 +473,41 @@ class TestSecureGroup:
             )
             mock_reschedule.assert_not_called()
 
+    @patch("xknx.io.ip_secure.SecureSequenceTimer._notify_timer_expired")
+    @patch("xknx.io.ip_secure.SecureSequenceTimer._monotonic_ms")
+    async def test_send_secure_wrapper_timer_overflow(
+        self,
+        mock_monotonic_ms,
+        _mock_notify_timer_expired,  # we don't want to actually send here
+        mock_super_send,
+        mock_super_connect,
+    ):
+        """Test raising when secure timer overflows."""
+        mock_monotonic_ms.return_value = 0xFF_FF_FF_FF_FF_FF + 1
+        secure_group = SecureGroup(
+            local_addr=self.mock_addr,
+            remote_addr=(DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT),
+            backbone_key=self.mock_backbone_key,
+            latency_ms=1000,
+        )
+        secure_timer = secure_group.secure_timer
+        secure_timer.timer_authenticated = True
+
+        raw_test_cemi = CEMIFrame(
+            code=CEMIMessageCode.L_DATA_IND,
+            data=CEMILData.init_from_telegram(
+                Telegram(
+                    destination_address=GroupAddress("1/2/3"),
+                    payload=apci.GroupValueRead(),
+                )
+            ),
+        ).to_knx()
+
+        with pytest.raises(IPSecureError):
+            secure_group.send(
+                KNXIPFrame.init_from_body(RoutingIndication(raw_cemi=raw_test_cemi))
+            )
+
     async def test_receive_plain_frames(
         self,
         mock_super_send,