Repository: XKNX/xknx
Pull request #1188 (2023-02-09 09:01:50)

Support requesting specific IA for TCP connections
<!--
  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.
-->
- Use only Interfaces listed in Keyring when `ConnectionType.AUTOMATIC` is used and a Keyring is configured.
- Request specific tunnel by individual address for TCP connections when `ConnectionConfig.individual_address` is set.

Fixes # (issue)

## 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)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code quality improvements to existing code or addition of tests

## Checklist

- [x] The documentation has been adjusted accordingly
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog (docs/changelog.md)

codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1188](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (2ec2498) into [main](https://codecov.io/gh/XKNX/xknx/commit/d9e94ea79b7ee9730e678eacfd638f133b33bde1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (d9e94ea) will **increase** coverage by `0.01%`.
> The diff coverage is `97.87%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/1188/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@            Coverage Diff             @@
##             main    #1188      +/-   ##
==========================================
+ Coverage   96.43%   96.44%   +0.01%     
==========================================
  Files         143      143              
  Lines        8900     8929      +29     
==========================================
+ Hits         8583     8612      +29     
  Misses        317      317              
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/io/connection.py](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9jb25uZWN0aW9uLnB5) | `100.00% <ø> (ø)` | |
| [xknx/knxip/connect\_request.py](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9jb25uZWN0X3JlcXVlc3QucHk=) | `98.03% <97.14%> (+1.16%)` | :arrow_up: |
| [xknx/io/knxip\_interface.py](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9rbnhpcF9pbnRlcmZhY2UucHk=) | `87.96% <100.00%> (+0.05%)` | :arrow_up: |
| [xknx/io/request\_response/connect.py](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9yZXF1ZXN0X3Jlc3BvbnNlL2Nvbm5lY3QucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/io/tunnel.py](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby90dW5uZWwucHk=) | `85.88% <100.00%> (+0.11%)` | :arrow_up: |
| [xknx/knxip/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/knxip/knxip\_enum.py](https://codecov.io/gh/XKNX/xknx/pull/1188?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9rbnhpcF9lbnVtLnB5) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index a88744f0..08288319 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -11,12 +11,18 @@ nav_order: 2
 ### Connection
 
 - Use only Interfaces listed in Keyring when `ConnectionType.AUTOMATIC` is used and a Keyring is configured.
+- Request specific tunnel by individual address for TCP connections when `ConnectionConfig.individual_address` is set.
 
 ### Bugfixes
 
 - Parse Data Secure credentials form Keyring from non-IP-Secure interfaces.
 - Parse Data Secure credentials from Keyrings exported for specific interfaces.
 
+### Protocol
+
+- Support Extended Connection Request Information (CRI) for requesting a specific individual address on Tunnelling v2.
+- Add Core v2 Error Code definitions.
+
 ### Cleanups
 
 - Accept `str | os.PathLike` for Keyring path. Previously only `str`.
diff --git a/docs/xknx.md b/docs/xknx.md
index 8e995450..92e9f98a 100644
--- a/docs/xknx.md
+++ b/docs/xknx.md
@@ -73,6 +73,8 @@ An explicit connection configuration can be used. In this case a `connection_typ
 KNX Data Secure credentials are sourced from a keyfile exported from ETS.
 IP Secure keys can be configured directly or sourced from a keyfile.
 
+For TCP tunnel connections a specific tunnel endpoint can be requested by setting `individual_address`. For SECURE tunnels this setting selects an interface from a given keyfile.
+
 # [](#header-2)Starting
 
 ```python
diff --git a/xknx/io/connection.py b/xknx/io/connection.py
index 7cd0bf8f..e01becc9 100644
--- a/xknx/io/connection.py
+++ b/xknx/io/connection.py
@@ -34,6 +34,7 @@ class ConnectionConfig:
         * TUNNELING_TCP connect to a specific KNX/IP tunneling v2 device via TCP.
     * individual address:
         * ROUTING the individual address used as source address for routing
+        * TCP TUNNELING request a specific tunnel endpoint
         * SECURE TUNNELING use a specific tunnel endpoint from the knxkeys file
     * local_ip: Local ip of the interface though which KNXIPInterface should connect.
     * gateway_ip: IP of KNX/IP tunneling device.
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index c04eb054..5e0ec5eb 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -184,15 +184,21 @@ class KNXIPInterface:
     ) -> None:
         """Start KNX/IP TCP tunnel."""
         util.validate_ip(gateway_ip, address_name="Gateway IP address")
+        tunnel_address = self.connection_config.individual_address
+
         logger.debug(
-            "Starting tunnel to %s:%s over TCP",
+            "Starting tunnel to %s:%s over TCP%s",
             gateway_ip,
             gateway_port,
+            f" requesting individual address {tunnel_address}"
+            if tunnel_address
+            else "",
         )
         self._interface = TCPTunnel(
             self.xknx,
             gateway_ip=gateway_ip,
             gateway_port=gateway_port,
+            individual_address=tunnel_address,
             cemi_received_callback=self.cemi_received,
             auto_reconnect=self.connection_config.auto_reconnect,
             auto_reconnect_wait=self.connection_config.auto_reconnect_wait,
diff --git a/xknx/io/request_response/connect.py b/xknx/io/request_response/connect.py
index 90f7dfd9..355d41d1 100644
--- a/xknx/io/request_response/connect.py
+++ b/xknx/io/request_response/connect.py
@@ -6,10 +6,11 @@ from typing import TYPE_CHECKING
 from xknx.knxip import (
     HPAI,
     ConnectRequest,
-    ConnectRequestType,
+    ConnectRequestInformation,
     ConnectResponse,
     KNXIPFrame,
 )
+from xknx.telegram import IndividualAddress
 
 from .request_response import RequestResponse
 
@@ -18,27 +19,34 @@ if TYPE_CHECKING:
 
 
 class Connect(RequestResponse):
-    """Class to send a ConnectRequest and wait for ConnectResponse."""
+    """
+    Class to send a ConnectRequest and wait for ConnectResponse.
+
+    Setting a `individual_address` is only supported for Tunnelling v2 connections.
+    """
 
     def __init__(
         self,
         transport: KNXIPTransport,
         local_hpai: HPAI,
+        individual_address: IndividualAddress | None = None,
     ):
         """Initialize Connect class."""
         super().__init__(transport, ConnectResponse)
         self.communication_channel = 0
         self.data_endpoint = HPAI()
         self.identifier = 0
+        self.individual_address = individual_address
         self.local_hpai = local_hpai
 
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
+        cri = ConnectRequestInformation(individual_address=self.individual_address)
         # use the same HPAI for control_endpoint and data_endpoint
         connect_request = ConnectRequest(
-            request_type=ConnectRequestType.TUNNEL_CONNECTION,
             control_endpoint=self.local_hpai,
             data_endpoint=self.local_hpai,
+            cri=cri,
         )
         return KNXIPFrame.init_from_body(connect_request)
 
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index 6925e620..285867e0 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -70,6 +70,7 @@ class _Tunnel(Interface):
         self._initial_connection = True
         self._is_reconnecting = False
         self._reconnect_task: asyncio.Task[None] | None = None
+        self._requested_address: IndividualAddress | None = None
         self._src_address = IndividualAddress(0)
         self._send_lock = asyncio.Lock()
         # self._tunnelling_request_confirmation_event = asyncio.Event()
@@ -186,7 +187,11 @@ class _Tunnel(Interface):
 
     async def _connect_request(self) -> bool:
         """Connect to tunnelling server. Set communication_channel and src_address."""
-        connect = Connect(transport=self.transport, local_hpai=self.local_hpai)
+        connect = Connect(
+            transport=self.transport,
+            local_hpai=self.local_hpai,
+            individual_address=self._requested_address,
+        )
         await connect.start()
         if connect.success:
             self.communication_channel = connect.communication_channel
@@ -557,6 +562,7 @@ class TCPTunnel(_Tunnel):
         cemi_received_callback: CEMICallbackType,
         gateway_ip: str,
         gateway_port: int,
+        individual_address: IndividualAddress | None = None,
         auto_reconnect: bool = True,
         auto_reconnect_wait: int = 3,
     ):
@@ -571,6 +577,7 @@ class TCPTunnel(_Tunnel):
         )
         # TCP always uses 0.0.0.0:0
         self.local_hpai = HPAI(protocol=HostProtocol.IPV4_TCP)
+        self._requested_address = individual_address
 
     def _init_transport(self) -> None:
         """Initialize transport transport."""
diff --git a/xknx/knxip/__init__.py b/xknx/knxip/__init__.py
index c92f44f5..07b0e121 100644
--- a/xknx/knxip/__init__.py
+++ b/xknx/knxip/__init__.py
@@ -1,7 +1,7 @@
 """This package contains all methods for serialization and deserialization of KNX/IP packets."""
 # flake8: noqa
 from .body import KNXIPBody, KNXIPBodyResponse
-from .connect_request import ConnectRequest
+from .connect_request import ConnectRequest, ConnectRequestInformation
 from .connect_response import ConnectResponse
 from .connectionstate_request import ConnectionStateRequest
 from .connectionstate_response import ConnectionStateResponse
@@ -29,6 +29,7 @@ from .knxip_enum import (
     KNXIPServiceType,
     KNXMedium,
     SearchRequestParameterType,
+    TunnellingLayer,
 )
 from .routing_busy import RoutingBusy
 from .routing_indication import RoutingIndication
@@ -51,6 +52,7 @@ __all__ = [
     "KNXIPBody",
     "KNXIPBodyResponse",
     "ConnectRequest",
+    "ConnectRequestInformation",
     "ConnectResponse",
     "ConnectionStateRequest",
     "ConnectionStateResponse",
@@ -90,5 +92,6 @@ __all__ = [
     "SRP",
     "TimerNotify",
     "TunnellingAck",
+    "TunnellingLayer",
     "TunnellingRequest",
 ]
diff --git a/xknx/knxip/connect_request.py b/xknx/knxip/connect_request.py
index 234fa76a..f8e56344 100644
--- a/xknx/knxip/connect_request.py
+++ b/xknx/knxip/connect_request.py
@@ -6,69 +6,46 @@ Connect requests are used to start a new tunnel connection on a KNX/IP device.
 from __future__ import annotations
 
 from xknx.exceptions import CouldNotParseKNXIP
+from xknx.telegram import IndividualAddress
 
 from .body import KNXIPBody
 from .hpai import HPAI
-from .knxip_enum import ConnectRequestType, KNXIPServiceType
+from .knxip_enum import ConnectRequestType, KNXIPServiceType, TunnellingLayer
 
 
 class ConnectRequest(KNXIPBody):
     """Representation of a KNX Connect Request."""
 
     SERVICE_TYPE = KNXIPServiceType.CONNECT_REQUEST
-    CRI_LENGTH = 4
 
     def __init__(
         self,
-        request_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
         control_endpoint: HPAI | None = None,
         data_endpoint: HPAI | None = None,
+        cri: ConnectRequestInformation | None = None,
     ):
         """Initialize ConnectRequest object."""
-        self.request_type = request_type
         self.control_endpoint = control_endpoint or HPAI()
         self.data_endpoint = data_endpoint or HPAI()
-        # KNX layer, 0x02 = TUNNEL_LINKLAYER
-        self.flags = 0x02
+        self.cri = cri or ConnectRequestInformation()
 
     def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
-        return HPAI.LENGTH + HPAI.LENGTH + ConnectRequest.CRI_LENGTH
+        return HPAI.LENGTH + HPAI.LENGTH + self.cri.calculated_length()
 
     def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
-
-        def cri_from_knx(cri: bytes) -> int:
-            """Parse CRI (Connection Request Information)."""
-            if cri[0] != ConnectRequest.CRI_LENGTH:
-                raise CouldNotParseKNXIP("CRI has wrong length")
-            if len(cri) < ConnectRequest.CRI_LENGTH:
-                raise CouldNotParseKNXIP("CRI data has wrong length")
-            self.request_type = ConnectRequestType(cri[1])
-            self.flags = cri[2]
-            return 4
-
         pos = self.control_endpoint.from_knx(raw)
         pos += self.data_endpoint.from_knx(raw[pos:])
-        pos += cri_from_knx(raw[pos:])
+        pos += self.cri.from_knx(raw[pos:])
         return pos
 
     def to_knx(self) -> bytes:
         """Serialize to KNX/IP raw data."""
-
-        def cri_to_knx() -> bytes:
-            """Serialize CRI (Connect Request Information)."""
-            return bytes(
-                (
-                    ConnectRequest.CRI_LENGTH,
-                    self.request_type.value,
-                    self.flags,
-                    0x00,  # Reserved
-                )
-            )
-
         return (
-            self.control_endpoint.to_knx() + self.data_endpoint.to_knx() + cri_to_knx()
+            self.control_endpoint.to_knx()
+            + self.data_endpoint.to_knx()
+            + self.cri.to_knx()
         )
 
     def __repr__(self) -> str:
@@ -77,6 +54,86 @@ class ConnectRequest(KNXIPBody):
             "<ConnectRequest "
             f'control_endpoint="{self.control_endpoint}" '
             f'data_endpoint="{self.data_endpoint}" '
-            f'request_type="{self.request_type}" '
-            f'flags="{hex(self.flags)}" />'
+            f'cri="{self.cri}" />'
+        )
+
+
+class ConnectRequestInformation:
+    """
+    Representation of a KNX Connect Request Information (CRI).
+
+    A Basic CRI requests a tunnel without requesting any specific IA.
+    Using `individual_address` yields an Extended CRI which is only
+    supported by Tunnelling v2 devices.
+    """
+
+    CRI_BASIC_LENGTH = 4
+    CRI_EXTENDED_LENGTH = 6
+
+    def __init__(
+        self,
+        connection_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
+        knx_layer: TunnellingLayer = TunnellingLayer.DATA_LINK_LAYER,
+        individual_address: IndividualAddress | None = None,
+    ):
+        """Initialize ConnectRequest object."""
+        self.connection_type = connection_type
+        self.knx_layer = knx_layer
+        self.individual_address = individual_address
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP body."""
+        return (
+            ConnectRequestInformation.CRI_EXTENDED_LENGTH
+            if self.individual_address
+            else ConnectRequestInformation.CRI_BASIC_LENGTH
+        )
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        cri_length = raw[0]
+        if cri_length == ConnectRequestInformation.CRI_BASIC_LENGTH:
+            extended = False
+        elif cri_length == ConnectRequestInformation.CRI_EXTENDED_LENGTH:
+            extended = True
+        else:
+            raise CouldNotParseKNXIP("CRI has wrong length")
+        if len(raw) < cri_length:
+            raise CouldNotParseKNXIP("CRI data has wrong length")
+        self.connection_type = ConnectRequestType(raw[1])
+        self.knx_layer = TunnellingLayer(raw[2])
+        if extended:
+            self.individual_address = IndividualAddress.from_knx(raw[4:6])
+        return cri_length
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        _cri = bytes(
+            (
+                self.calculated_length(),
+                self.connection_type.value,
+                self.knx_layer.value,
+                0x00,  # Reserved
+            )
+        )
+        if self.individual_address is None:
+            return _cri
+        return _cri + self.individual_address.to_knx()
+
+    def __eq__(self, other: object) -> bool:
+        """Equal operator."""
+        return self.__dict__ == other.__dict__
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        _extended = (
+            f'individual_address="{self.individual_address}" '
+            if self.individual_address
+            else ""
+        )
+        return (
+            "<ConnectRequestInformation "
+            f'connection_type="{self.connection_type.name}" '
+            f'knx_layer="{self.knx_layer.name}" '
+            f"{_extended}/>"
         )
diff --git a/xknx/knxip/error_code.py b/xknx/knxip/error_code.py
index 68444ce3..349b4f2a 100644
--- a/xknx/knxip/error_code.py
+++ b/xknx/knxip/error_code.py
@@ -17,6 +17,10 @@ class ErrorCode(Enum):
     # received sequence number is out of order.
     E_SEQUENCE_NUMBER = 0x04
 
+    # Any further undefined, possibly implementation specific error has occurred.
+    # Core v2
+    E_ERROR = 0x0F
+
     # The KNXnet/IP Server device cannot find an active data
     # connection with the specified ID.
     E_CONNECTION_ID = 0x21
@@ -44,6 +48,18 @@ class ErrorCode(Enum):
     # the KNX subnetwork connection with the specified ID.
     E_KNX_CONNECTION = 0x27
 
+    # The Client is not authorised to use the requested IA in the Extended CRI.
+    # Core v2
+    E_AUTHORISATION_ERROR = 0x28
+
     # The requested tunnelling layer is not supported by the
     # KNXnet/IP Server device.
     E_TUNNELLING_LAYER = 0x29
+
+    # The IA requested in the Extended CRI is not a Tunnelling IA.
+    # Core v2
+    E_NO_TUNNELLING_ADDRESS = 0x2D
+
+    # The IA requested for this connection is in use.
+    # Core v2
+    E_CONNECTION_IN_USE = 0x2E
diff --git a/xknx/knxip/knxip_enum.py b/xknx/knxip/knxip_enum.py
index ac5eac32..0ab5e94d 100644
--- a/xknx/knxip/knxip_enum.py
+++ b/xknx/knxip/knxip_enum.py
@@ -54,47 +54,48 @@ class ConnectRequestType(Enum):
 
     # Data connection used to configure a KNXnet/IP device
     DEVICE_MGMT_CONNECTION = 0x03
-
     # Data connection used to forward KNX telegrams between
     # two KNXnet/IP devices.
     TUNNEL_CONNECTION = 0x04
-
     # Data connection used for configuration and data transfer
     # with a remote logging server.
     REMLOG_CONNECTION = 0x06
-
     # Data connection used for data transfer with a remote
     # configuration server.
     REMCONF_CONNECTION = 0x07
-
     # Data connection used for configuration and data transfer
     # with an Object Server in a KNXnet/IP device.
     OBJSVR_CONNECTION = 0x08
 
 
+class TunnellingLayer(Enum):
+    """Enum class for KNX/IP Tunnelling Layer."""
+
+    # Data Link Layer tunnel
+    DATA_LINK_LAYER = 0x02
+    # Raw tunnel
+    RAW_LAYER = 0x04
+    # Busmonitor tunnel
+    BUSMONITOR_LAYER = 0x80
+
+
 class DIBTypeCode(Enum):
     """Enum class for KNX/IP DIB Type Codes."""
 
     # Device information e.g. KNX medium.
     DEVICE_INFO = 0x01
-
     # Service families supported by the device.
     SUPP_SVC_FAMILIES = 0x02
-
     # IP configuration
     IP_CONFIG = 0x03
-
     # current configuration
     IP_CUR_CONFIG = 0x04
-
     # KNX addresses
     KNX_ADDRESSES = 0x05
-
     # KNX IP Secure
     SECURED_SERVICE_FAMILIES = 0x06
     TUNNELING_INFO = 0x07
     ADDITIONAL_DEVICE_INFO = 0x08
-
     # DIB structure for further data defined by device manufacturer.
     MFR_DATA = 0xFE
 
@@ -120,25 +121,18 @@ class DIBServiceFamily(Enum):
 
     #  Core
     CORE = 0x02
-
     # Device Management
     DEVICE_MANAGEMENT = 0x03
-
     # Tunnelling
     TUNNELING = 0x04
-
     # Routing
     ROUTING = 0x05
-
     # Remote Logging
     REMOTE_LOGGING = 0x06
-
     # Configuration and Diagnosis
     REMOTE_CONFIGURATION_DIAGNOSIS = 0x07
-
     # Object Server'.
     OBJECT_SERVER = 0x08
-
     # Security - Extended search response only
     SECURITY = 0x09
 
@@ -165,15 +159,11 @@ class SearchRequestParameterType(Enum):
 
     # Used to test behavior of the KNXnet/IP server for unknown SRPs. Don't use!
     INVALID = 0x00
-
     # Select only KNXnet/IP Servers that are currently in programming mode
     SELECT_BY_PROGRAMMING_MODE = 0x01
-
     # Select only KNXnet/IP Servers that have the given MAC address
     SELECT_BY_MAC_ADDRESS = 0x02
-
     # Select only KNXnet/IP Servers that support the given DIBServiceFamily in a given version
     SELECT_BY_SERVICE = 0x03
-
     # The Client shall include this SRP to indicate that it is interested in the listed DIBs
     REQUEST_DIBS = 0x04


Test patch:
diff --git a/test/io_tests/knxip_interface_test.py b/test/io_tests/knxip_interface_test.py
index c1e0dc20..0b3bb322 100644
--- a/test/io_tests/knxip_interface_test.py
+++ b/test/io_tests/knxip_interface_test.py
@@ -181,6 +181,38 @@ class TestKNXIPInterface:
             )
             connect_tcp.assert_called_once_with()
 
+    async def test_start_tcp_tunnel_connection_with_ia(self):
+        """Test starting TCP tunnel connection requesting specific tunnel."""
+        # without gateway_ip automatic is called
+        gateway_ip = "127.0.0.2"
+        connection_config = ConnectionConfig(
+            connection_type=ConnectionType.TUNNELING_TCP,
+            gateway_ip=gateway_ip,
+            individual_address="1.1.1",
+        )
+        with patch(
+            "xknx.io.KNXIPInterface._start_tunnelling_tcp"
+        ) as start_tunnelling_tcp:
+            interface = knx_interface_factory(self.xknx, connection_config)
+            await interface.start()
+            start_tunnelling_tcp.assert_called_once_with(
+                gateway_ip=gateway_ip,
+                gateway_port=3671,
+            )
+        with patch("xknx.io.tunnel.TCPTunnel.connect") as connect_tcp:
+            interface = knx_interface_factory(self.xknx, connection_config)
+            await interface.start()
+            assert isinstance(interface._interface, TCPTunnel)
+            assert interface._interface.gateway_ip == gateway_ip
+            assert interface._interface.gateway_port == 3671
+            assert interface._interface._requested_address == IndividualAddress("1.1.1")
+            assert interface._interface.auto_reconnect
+            assert interface._interface.auto_reconnect_wait == 3
+            assert (  # pylint: disable=comparison-with-callable
+                interface._interface.cemi_received_callback == interface.cemi_received
+            )
+            connect_tcp.assert_called_once_with()
+
     async def test_start_routing_connection(self):
         """Test starting routing connection."""
         local_ip = "127.0.0.1"
diff --git a/test/io_tests/request_response_tests/connect_test.py b/test/io_tests/request_response_tests/connect_test.py
index f76cf6f5..6c77b373 100644
--- a/test/io_tests/request_response_tests/connect_test.py
+++ b/test/io_tests/request_response_tests/connect_test.py
@@ -7,7 +7,6 @@ from xknx.knxip import (
     HPAI,
     ConnectionStateRequest,
     ConnectRequest,
-    ConnectRequestType,
     ConnectResponse,
     ErrorCode,
     KNXIPFrame,
@@ -29,7 +28,6 @@ class TestConnect:
         # Expected KNX/IP-Frame:
         exp_knxipframe = KNXIPFrame.init_from_body(
             ConnectRequest(
-                request_type=ConnectRequestType.TUNNEL_CONNECTION,
                 control_endpoint=local_hpai,
                 data_endpoint=local_hpai,
             )
@@ -83,9 +81,7 @@ class TestConnect:
         assert connect.awaited_response_class == ConnectResponse
 
         # Expected KNX/IP-Frame:
-        exp_knxipframe = KNXIPFrame.init_from_body(
-            ConnectRequest(request_type=ConnectRequestType.TUNNEL_CONNECTION)
-        )
+        exp_knxipframe = KNXIPFrame.init_from_body(ConnectRequest())
         with patch("xknx.io.transport.UDPTransport.send") as mock_udp_send, patch(
             "xknx.io.transport.UDPTransport.getsockname"
         ) as mock_udp_getsockname:
diff --git a/test/knxip_tests/connect_request_test.py b/test/knxip_tests/connect_request_test.py
index 12ffca75..13db0894 100644
--- a/test/knxip_tests/connect_request_test.py
+++ b/test/knxip_tests/connect_request_test.py
@@ -2,7 +2,15 @@
 import pytest
 
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import HPAI, ConnectRequest, ConnectRequestType, KNXIPFrame
+from xknx.knxip import (
+    HPAI,
+    ConnectRequest,
+    ConnectRequestInformation,
+    ConnectRequestType,
+    KNXIPFrame,
+    TunnellingLayer,
+)
+from xknx.telegram import IndividualAddress
 
 
 class TestKNXIPConnectRequest:
@@ -17,16 +25,58 @@ class TestKNXIPConnectRequest:
         knxipframe, _ = KNXIPFrame.from_knx(raw)
 
         assert isinstance(knxipframe.body, ConnectRequest)
-        assert knxipframe.body.request_type == ConnectRequestType.TUNNEL_CONNECTION
         assert knxipframe.body.control_endpoint == HPAI(
             ip_addr="192.168.42.1", port=33941
         )
         assert knxipframe.body.data_endpoint == HPAI(ip_addr="192.168.42.1", port=52393)
+        assert (
+            knxipframe.body.cri.connection_type is ConnectRequestType.TUNNEL_CONNECTION
+        )
+        assert knxipframe.body.cri.knx_layer is TunnellingLayer.DATA_LINK_LAYER
+        assert knxipframe.body.cri.individual_address is None
+
+        cri = ConnectRequestInformation(
+            connection_type=ConnectRequestType.TUNNEL_CONNECTION,
+            knx_layer=TunnellingLayer.DATA_LINK_LAYER,
+            individual_address=None,
+        )
+        connect_request = ConnectRequest(
+            control_endpoint=HPAI(ip_addr="192.168.42.1", port=33941),
+            data_endpoint=HPAI(ip_addr="192.168.42.1", port=52393),
+            cri=cri,
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connect_request)
+
+        assert knxipframe2.to_knx() == raw
 
+    def test_connect_request_extended_cri(self):
+        """Test parsing and streaming connection request KNX/IP packet."""
+        raw = bytes.fromhex(
+            "06 10 02 05 00 1C 08 01 C0 A8 2A 01 84 95 08 01"
+            "C0 A8 2A 01 CC A9 06 04 02 00 01 02"
+        )
+        knxipframe, _ = KNXIPFrame.from_knx(raw)
+
+        assert isinstance(knxipframe.body, ConnectRequest)
+        assert knxipframe.body.control_endpoint == HPAI(
+            ip_addr="192.168.42.1", port=33941
+        )
+        assert knxipframe.body.data_endpoint == HPAI(ip_addr="192.168.42.1", port=52393)
+        assert (
+            knxipframe.body.cri.connection_type is ConnectRequestType.TUNNEL_CONNECTION
+        )
+        assert knxipframe.body.cri.knx_layer is TunnellingLayer.DATA_LINK_LAYER
+        assert knxipframe.body.cri.individual_address == IndividualAddress("0.1.2")
+
+        cri = ConnectRequestInformation(
+            connection_type=ConnectRequestType.TUNNEL_CONNECTION,
+            knx_layer=TunnellingLayer.DATA_LINK_LAYER,
+            individual_address=IndividualAddress("0.1.2"),
+        )
         connect_request = ConnectRequest(
-            request_type=ConnectRequestType.TUNNEL_CONNECTION,
             control_endpoint=HPAI(ip_addr="192.168.42.1", port=33941),
             data_endpoint=HPAI(ip_addr="192.168.42.1", port=52393),
+            cri=cri,
         )
         knxipframe2 = KNXIPFrame.init_from_body(connect_request)
 
diff --git a/test/str_test.py b/test/str_test.py
index 3e9af0e0..8ad71aec 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -526,7 +526,7 @@ class TestStringRepresentations:
         assert (
             str(connect_request)
             == '<ConnectRequest control_endpoint="192.168.42.1:33941/udp" data_endpoint="192.168.42.2:33942/udp" '
-            'request_type="ConnectRequestType.TUNNEL_CONNECTION" flags="0x2" />'
+            'cri="<ConnectRequestInformation connection_type="TUNNEL_CONNECTION" knx_layer="DATA_LINK_LAYER" />" />'
         )
 
     def test_connect_response(self):