Repository: XKNX/xknx (python)

Issue:
Can't connect to Knx gateway behind NAT on VM
I am trying to connect to a knx ip gateway from a homeassistant vm but get a connection error. The vm is behind a NAT on the host.

KNX Gateway IP: 192.168.2.191
Host IP: 192.168.2.249
VM IP: 192.168.1.11

My homeassistant configuration
 <pre>Tunneling:
    Host: '192.168.2.191'
    Port: 3671
    Local_ip: '192.168.1.11'</pre>

Iptables configuration on the host
<pre> echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A PREROUTING -s '192.168.2.191' -j DNAT --to-destination '192.168.1.11' 
iptables -t nat -A POSTROUTING -d '192.168.2.191' -j SNAT --to-source '192.168.2.249' </pre>

On tcpdump I see an UDP packet going to Knx gateway but there is no reply packet.

Pull request description:
Route back
<!--
  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.
-->
Quote from KNX specification:

> For special communication scenarios it is not suitable to put information about IP Host Protocol addresses into the payload of KNXnet/IP Frames.
If e.g. the Frame shall travel across IP routers performing network address translation (NAT), the IP Host Protocol address information that the KNXnet/IP Server needs to address to send KNXnet/IP Frames back to the KNXnet/IP Client may not be known to the KNXnet/IP Client.
For these scenarios a special encoding of the HPAI with the IP address and IP port number all set to zero shall be used. Such an HPAI is called a “Route Back” HPAI.
For UDP “Route Back” HPAIs this means that the KNXnet/IP Server shall use the IP address and the port number in the IP package received as the target IP address or port number for the response to the KNXnet/IP Client.

Fixes #215 

## 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)
- [x] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [x] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


Patch:
diff --git a/changelog.md b/changelog.md
index b8648063..eefef6fa 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,11 @@
 
 ## Unreleased changes
 
+### New Features
+
+- Add new optional config `route_back` for connections to be able to work behind NAT.
+- Read env vars after reading config file to allow dynamic config.
+
 ### HA integration
 
 - knx_event: fire also for outgoing telegrams
@@ -19,6 +24,7 @@
 - return the payload (or None) in RemoteValue.payload_valid(payload) instead of bool
 - Light colors are represented as `Tuple[Tuple[int,int,int], int]` instead of `Tuple[List[int], int]` now
 - DPT 3 payloads/values are not invertable anymore.
+- Tunnel: Interface changed - gateway_ip, gateway_port before local_ip, local_port added with default `0`.
 
 ## 0.16.3 Fan contributions 2021-02-06
 
diff --git a/docs/configuration.md b/docs/configuration.md
index 79b4ffc0..598c3a79 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -24,6 +24,7 @@ The configuration file can contain three sections.
     - `gateway_ip` (required) sets the ip address of the KNX tunneling interface
     - `gateway_port` (optional) sets the port the KNX tunneling interface is listening on
     - `local_ip` (optional) sets the ip address that is used by xknx
+    - `route_back` (optional) When True the KNXnet/IP Server shall use the IP address and the port number from the IP package received as the target IP address or port number for the response to the KNXnet/IP Client (for NAT / Docker). Defalut: `False`
   - `routing` for a UDP multicast connection
     - `local_ip` (optional) sets the ip address that is used by xknx
 - Within the `groups` sections all devices are defined. For each type of device more then one section might be specified. You need to append numbers or strings to differentiate the entries, as in the example below. The appended number or string must be unique.
diff --git a/docs/xknx.md b/docs/xknx.md
index 28670162..e30bdda2 100644
--- a/docs/xknx.md
+++ b/docs/xknx.md
@@ -146,5 +146,27 @@ async def main():
 asyncio.run(main())
 ```
 
-
-
+# [](#header-2)Dockerised xknx's app
+
+To run xknx from a container, set 'route_back=true' or use host network mode.
+
+Available env variables are:
+- XKNX_GENERAL_OWN_ADDRESS
+- XKNX_GENERAL_RATE_LIMIT
+- XKNX_GENERAL_MULTICAST_GROUP
+- XKNX_GENERAL_MULTICAST_PORT
+- XKNX_CONNECTION_GATEWAY_IP: Your KNX Gateway IP address
+- XKNX_CONNECTION_GATEWAY_PORT: Your KNX Gateway UDP port
+- XKNX_CONNECTION_LOCAL_IP
+- XKNX_CONNECTION_ROUTE_BACK: Set 'true' to be able to work in a container
+
+Example of a `docker run` with an xknx based app:
+
+```bash
+docker run --name myapp -d \
+  -e XKNX_CONNECTION_GATEWAY_IP='192.168.0.123' \
+  -e XKNX_CONNECTION_LOCAL_PORT=12399 \
+  -e XKNX_CONNECTION_ROUTE_BACK=true \
+  -p 12300:12399/udp \
+  myapp:latest
+```
diff --git a/examples/example_tunnel.py b/examples/example_tunnel.py
index 86a83c00..dc8dd299 100644
--- a/examples/example_tunnel.py
+++ b/examples/example_tunnel.py
@@ -30,9 +30,11 @@ async def main():
 
     tunnel = Tunnel(
         xknx,
-        local_ip=gateway.local_ip,
         gateway_ip=gateway.ip_addr,
         gateway_port=gateway.port,
+        local_ip=gateway.local_ip,
+        local_port=0,
+        route_back=False,
     )
 
     await tunnel.connect()
diff --git a/xknx.yaml b/xknx.yaml
index f8be7300..a1a60161 100644
--- a/xknx.yaml
+++ b/xknx.yaml
@@ -5,7 +5,15 @@ general:
     multicast_port: 1337
 
 connection:
+    # One of them: auto, tunneling or routing
     auto:
+    #tunneling:
+    #    local_ip: "192.168.111.201"
+    #    route_back: false
+    #    gateway_ip: "192.168.0.202"
+    #    gateway_port: 3671
+    #routing:
+    #    local_ip: "192.168.111.201"
 
 groups:
     binary_sensor:
diff --git a/xknx/config/config_v1.py b/xknx/config/config_v1.py
index 02b9dfd2..f8e84f78 100644
--- a/xknx/config/config_v1.py
+++ b/xknx/config/config_v1.py
@@ -6,6 +6,7 @@ Module for reading config files (xknx.yaml).
 """
 from enum import Enum
 import logging
+import os
 
 from xknx.devices import (
     BinarySensor,
@@ -43,10 +44,12 @@ class ConfigV1:
         self.xknx = xknx
 
     def parse(self, doc):
-        """Parse the config."""
+        """Parse the config and read ENV vars."""
         self.parse_general(doc)
         self.parse_connection(doc)
         self.parse_groups(doc)
+        self.env_general()
+        self.env_connection()
 
     def parse_general(self, doc):
         """Parse the general section of xknx.yaml."""
@@ -60,6 +63,25 @@ class ConfigV1:
             if "multicast_port" in doc["general"]:
                 self.xknx.multicast_port = doc["general"]["multicast_port"]
 
+    def env_general(self):
+        """Get Env Vars for the general section."""
+        own_address = os.getenv("XKNX_GENERAL_OWN_ADDRESS", default=None)
+        if own_address:
+            logger.debug("XKNX_GENERAL_OWN_ADDRESS overwrite from env")
+            self.xknx.own_address = IndividualAddress(own_address)
+        rate_limit = os.getenv("XKNX_GENERAL_RATE_LIMIT", default=None)
+        if rate_limit:
+            logger.debug("XKNX_GENERAL_RATE_LIMIT overwrite from env")
+            self.xknx.rate_limit = int(rate_limit)
+        multicast_group = os.getenv("XKNX_GENERAL_MULTICAST_GROUP", default=None)
+        if multicast_group:
+            logger.debug("XKNX_GENERAL_MULTICAST_GROUP overwrite from env")
+            self.xknx.multicast_group = multicast_group
+        multicast_port = os.getenv("XKNX_GENERAL_MULTICAST_PORT", default=None)
+        if multicast_port:
+            logger.debug("XKNX_GENERAL_MULTICAST_PORT overwrite from env")
+            self.xknx.multicast_port = int(multicast_port)
+
     def parse_connection(self, doc):
         """Parse the connection section of xknx.yaml."""
         if "connection" in doc and hasattr(doc["connection"], "__iter__"):
@@ -94,8 +116,35 @@ class ConfigV1:
                     connection_config.gateway_port = value
                 elif pref == "local_ip":
                     connection_config.local_ip = value
+                elif pref == "route_back":
+                    connection_config.route_back = value
         self.xknx.connection_config = connection_config
 
+    def env_connection(self):
+        """Read config from env vars."""
+        gateway_ip = os.getenv("XKNX_CONNECTION_GATEWAY_IP", default=None)
+        if gateway_ip:
+            logger.debug("XKNX_CONNECTION_GATEWAY_IP overwrite from env")
+            self.xknx.connection_config.gateway_ip = gateway_ip
+        gateway_port = os.getenv("XKNX_CONNECTION_GATEWAY_PORT", default=None)
+        if gateway_port:
+            logger.debug("XKNX_CONNECTION_GATEWAY_PORT overwrite from env")
+            self.xknx.connection_config.gateway_port = int(gateway_port)
+        local_ip = os.getenv("XKNX_CONNECTION_LOCAL_IP", default=None)
+        if local_ip:
+            logger.debug("XKNX_CONNECTION_LOCAL_IP overwrite from env")
+            self.xknx.connection_config.local_ip = local_ip
+        route_back = os.getenv("XKNX_CONNECTION_ROUTE_BACK", default=None)
+        if route_back:
+            logger.debug("XKNX_CONNECTION_ROUTE_BACK overwrite from env")
+            self.xknx.connection_config.route_back = route_back.lower() in [
+                "true",
+                "1",
+                "y",
+                "yes",
+                "on",
+            ]
+
     def parse_groups(self, doc):
         """Parse the group section of xknx.yaml."""
         if "groups" in doc and hasattr(doc["groups"], "__iter__"):
diff --git a/xknx/io/connect.py b/xknx/io/connect.py
index 7cefcef5..b84069e5 100644
--- a/xknx/io/connect.py
+++ b/xknx/io/connect.py
@@ -20,18 +20,22 @@ if TYPE_CHECKING:
 class Connect(RequestResponse):
     """Class to send a ConnectRequest and wait for ConnectResponse.."""
 
-    def __init__(self, xknx: "XKNX", udp_client: "UDPClient"):
+    def __init__(self, xknx: "XKNX", udp_client: "UDPClient", route_back: bool = False):
         """Initialize Connect class."""
         self.udp_client = udp_client
+        self.route_back = route_back
         super().__init__(xknx, self.udp_client, ConnectResponse)
         self.communication_channel = 0
         self.identifier = 0
 
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
-        (local_addr, local_port) = self.udp_client.getsockname()
         # set control_endpoint and data_endpoint to the same udp_connection
-        endpoint = HPAI(ip_addr=local_addr, port=local_port)
+        if self.route_back:
+            endpoint = HPAI()
+        else:
+            (local_addr, local_port) = self.udp_client.getsockname()
+            endpoint = HPAI(ip_addr=local_addr, port=local_port)
         connect_request = ConnectRequest(
             self.xknx,
             request_type=ConnectRequestType.TUNNEL_CONNECTION,
diff --git a/xknx/io/connectionstate.py b/xknx/io/connectionstate.py
index 599ec0ec..8948b8a5 100644
--- a/xknx/io/connectionstate.py
+++ b/xknx/io/connectionstate.py
@@ -16,10 +16,15 @@ class ConnectionState(RequestResponse):
     """Class to send ConnectonStateRequest and wait for ConnectionStateResponse."""
 
     def __init__(
-        self, xknx: "XKNX", udp_client: "UDPClient", communication_channel_id: int
+        self,
+        xknx: "XKNX",
+        udp_client: "UDPClient",
+        communication_channel_id: int,
+        route_back: bool = False,
     ):
         """Initialize ConnectionState class."""
         self.udp_client = udp_client
+        self.route_back = route_back
         super().__init__(
             xknx,
             self.udp_client,
@@ -30,10 +35,14 @@ class ConnectionState(RequestResponse):
 
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
-        (local_addr, local_port) = self.udpclient.getsockname()
+        if self.route_back:
+            endpoint = HPAI()
+        else:
+            (local_addr, local_port) = self.udpclient.getsockname()
+            endpoint = HPAI(ip_addr=local_addr, port=local_port)
         connectionstate_request = ConnectionStateRequest(
             self.xknx,
             communication_channel_id=self.communication_channel_id,
-            control_endpoint=HPAI(ip_addr=local_addr, port=local_port),
+            control_endpoint=endpoint,
         )
         return KNXIPFrame.init_from_body(connectionstate_request)
diff --git a/xknx/io/disconnect.py b/xknx/io/disconnect.py
index d01caeda..df5bef40 100644
--- a/xknx/io/disconnect.py
+++ b/xknx/io/disconnect.py
@@ -15,20 +15,29 @@ class Disconnect(RequestResponse):
     """Class to send a DisconnectRequest and wait for a DisconnectResponse."""
 
     def __init__(
-        self, xknx: "XKNX", udp_client: "UDPClient", communication_channel_id: int
+        self,
+        xknx: "XKNX",
+        udp_client: "UDPClient",
+        communication_channel_id: int,
+        route_back: bool = False,
     ):
         """Initialize Disconnect class."""
         self.xknx = xknx
         self.udp_client = udp_client
+        self.route_back = route_back
         super().__init__(xknx, self.udp_client, DisconnectResponse)
         self.communication_channel_id = communication_channel_id
 
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
-        (local_addr, local_port) = self.udpclient.getsockname()
+        if self.route_back:
+            endpoint = HPAI()
+        else:
+            (local_addr, local_port) = self.udpclient.getsockname()
+            endpoint = HPAI(ip_addr=local_addr, port=local_port)
         disconnect_request = DisconnectRequest(
             self.xknx,
             communication_channel_id=self.communication_channel_id,
-            control_endpoint=HPAI(ip_addr=local_addr, port=local_port),
+            control_endpoint=endpoint,
         )
         return KNXIPFrame.init_from_body(disconnect_request)
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index 67744205..510b17b2 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -48,6 +48,8 @@ class ConnectionConfig:
     * local_ip: Local ip of the interface though which KNXIPInterface should connect.
     * gateway_ip: IP of KNX/IP tunneling device.
     * gateway_port: Port of KNX/IP tunneling device.
+    * route_back: the KNXnet/IP Server shall use the IP address and the port number in the IP package
+        received as the target IP address or port number for the response to the KNXnet/IP Client.
     * auto_reconnect: Auto reconnect to KNX/IP tunneling device if connection cannot be established.
     * auto_reconnect_wait: Wait n seconds before trying to reconnect to KNX/IP tunneling device.
     * scan_filter: For AUTOMATIC connection, limit scan with the given filter
@@ -59,8 +61,10 @@ class ConnectionConfig:
         self,
         connection_type: ConnectionType = ConnectionType.AUTOMATIC,
         local_ip: Optional[str] = None,
+        local_port: int = 0,
         gateway_ip: Optional[str] = None,
         gateway_port: int = DEFAULT_MCAST_PORT,
+        route_back: bool = False,
         auto_reconnect: bool = False,
         auto_reconnect_wait: int = 3,
         scan_filter: GatewayScanFilter = GatewayScanFilter(),
@@ -69,8 +73,10 @@ class ConnectionConfig:
         # pylint: disable=too-many-arguments
         self.connection_type = connection_type
         self.local_ip = local_ip
+        self.local_port = local_port
         self.gateway_ip = gateway_ip
         self.gateway_port = gateway_port
+        self.route_back = route_back
         self.auto_reconnect = auto_reconnect
         self.auto_reconnect_wait = auto_reconnect_wait
         if connection_type == ConnectionType.TUNNELING:
@@ -109,11 +115,13 @@ class KNXIPInterface:
             and self.connection_config.gateway_ip is not None
         ):
             await self.start_tunnelling(
-                self.connection_config.local_ip,
-                self.connection_config.gateway_ip,
-                self.connection_config.gateway_port,
-                self.connection_config.auto_reconnect,
-                self.connection_config.auto_reconnect_wait,
+                local_ip=self.connection_config.local_ip,
+                local_port=self.connection_config.local_port,
+                gateway_ip=self.connection_config.gateway_ip,
+                gateway_port=self.connection_config.gateway_port,
+                auto_reconnect=self.connection_config.auto_reconnect,
+                auto_reconnect_wait=self.connection_config.auto_reconnect_wait,
+                route_back=self.connection_config.route_back,
             )
         else:
             await self.start_automatic(self.connection_config.scan_filter)
@@ -135,10 +143,12 @@ class KNXIPInterface:
         if gateway.supports_tunnelling and scan_filter.routing is not True:
             await self.start_tunnelling(
                 local_interface_ip,
+                self.connection_config.local_port,
                 gateway.ip_addr,
                 gateway.port,
                 self.connection_config.auto_reconnect,
                 self.connection_config.auto_reconnect_wait,
+                route_back=self.connection_config.route_back,
             )
         elif gateway.supports_routing:
             await self.start_routing(local_interface_ip)
@@ -146,10 +156,12 @@ class KNXIPInterface:
     async def start_tunnelling(
         self,
         local_ip: Optional[str],
+        local_port: int,
         gateway_ip: str,
         gateway_port: int,
         auto_reconnect: bool,
         auto_reconnect_wait: int,
+        route_back: bool,
     ) -> None:
         """Start KNX/IP tunnel."""
         # pylint: disable=too-many-arguments
@@ -158,13 +170,19 @@ class KNXIPInterface:
             local_ip = self.find_local_ip(gateway_ip=gateway_ip)
         validate_ip(local_ip, address_name="Local IP address")
         logger.debug(
-            "Starting tunnel from %s to %s:%s", local_ip, gateway_ip, gateway_port
+            "Starting tunnel from %s:%s to %s:%s",
+            local_ip,
+            local_port,
+            gateway_ip,
+            gateway_port,
         )
         self.interface = Tunnel(
             self.xknx,
-            local_ip=local_ip,
             gateway_ip=gateway_ip,
             gateway_port=gateway_port,
+            local_ip=local_ip,
+            local_port=local_port,
+            route_back=route_back,
             telegram_received_callback=self.telegram_received,
             auto_reconnect=auto_reconnect,
             auto_reconnect_wait=auto_reconnect_wait,
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index bc0d527b..3ac8e8e1 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -43,9 +43,11 @@ class Tunnel(Interface):
     def __init__(
         self,
         xknx: "XKNX",
-        local_ip: str,
         gateway_ip: str,
         gateway_port: int,
+        local_ip: str,
+        local_port: int = 0,
+        route_back: bool = False,
         telegram_received_callback: Optional["TelegramCallbackType"] = None,
         auto_reconnect: bool = False,
         auto_reconnect_wait: int = 3,
@@ -53,9 +55,11 @@ class Tunnel(Interface):
         """Initialize Tunnel class."""
         # pylint: disable=too-many-arguments
         self.xknx = xknx
-        self.local_ip = local_ip
         self.gateway_ip = gateway_ip
         self.gateway_port = gateway_port
+        self.local_ip = local_ip
+        self.local_port = local_port
+        self.route_back = route_back
         self.telegram_received_callback = telegram_received_callback
 
         self.udp_client: UDPClient
@@ -77,7 +81,9 @@ class Tunnel(Interface):
     def init_udp_client(self) -> None:
         """Initialize udp_client."""
         self.udp_client = UDPClient(
-            self.xknx, (self.local_ip, 0), (self.gateway_ip, self.gateway_port)
+            self.xknx,
+            (self.local_ip, self.local_port),
+            (self.gateway_ip, self.gateway_port),
         )
 
         self.udp_client.register_callback(
@@ -158,7 +164,7 @@ class Tunnel(Interface):
 
     async def _connect_request(self) -> bool:
         """Connect to tunnelling server. Set communication_channel and src_address."""
-        connect = Connect(self.xknx, self.udp_client)
+        connect = Connect(self.xknx, self.udp_client, route_back=self.route_back)
         await connect.start()
         if connect.success:
             self.communication_channel = connect.communication_channel
@@ -182,6 +188,7 @@ class Tunnel(Interface):
             self.xknx,
             self.udp_client,
             communication_channel_id=self.communication_channel,
+            route_back=self.route_back,
         )
         await conn_state.start()
         return conn_state.success
@@ -193,6 +200,7 @@ class Tunnel(Interface):
                 self.xknx,
                 self.udp_client,
                 communication_channel_id=self.communication_channel,
+                route_back=self.route_back,
             )
             await disconnect.start()
             if not disconnect.success and not ignore_error:
diff --git a/xknx_v2.yaml b/xknx_v2.yaml
index f564d212..7693a1cc 100644
--- a/xknx_v2.yaml
+++ b/xknx_v2.yaml
@@ -8,7 +8,8 @@ multicast_group: '224.1.2.3'
 multicast_port: 1337
 connection: # optional
   type: tunneling # or routing|auto
-  local_ip: "192.168.0.201"
+  local_ip: "192.168.111.201"
+  route_back: false
   host: "192.168.0.202"
   port: 1337
 binary_sensor:


Test patch:
diff --git a/test/config_tests/config_v1_test.py b/test/config_tests/config_v1_test.py
index d0d224fe..f428ea6c 100644
--- a/test/config_tests/config_v1_test.py
+++ b/test/config_tests/config_v1_test.py
@@ -1,5 +1,6 @@
 """Unit test for Configuration logic."""
 import asyncio
+import os
 import unittest
 from unittest.mock import patch
 
@@ -608,3 +609,113 @@ class TestConfig(unittest.TestCase):
             mock_parse.side_effect = XKNXException()
             XKNX(config="xknx.yaml")
             self.assertEqual(mock_err.call_count, 1)
+
+    XKNX_GENERAL_OWN_ADDRESS = "1.2.3"
+    XKNX_GENERAL_RATE_LIMIT = "20"
+    XKNX_GENERAL_MULTICAST_GROUP = "1.2.3.4"
+    XKNX_GENERAL_MULTICAST_PORT = "1111"
+
+    def test_config_general_from_env(self):
+        os.environ["XKNX_GENERAL_OWN_ADDRESS"] = self.XKNX_GENERAL_OWN_ADDRESS
+        os.environ["XKNX_GENERAL_RATE_LIMIT"] = self.XKNX_GENERAL_RATE_LIMIT
+        os.environ["XKNX_GENERAL_MULTICAST_GROUP"] = self.XKNX_GENERAL_MULTICAST_GROUP
+        os.environ["XKNX_GENERAL_MULTICAST_PORT"] = self.XKNX_GENERAL_MULTICAST_PORT
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_GENERAL_OWN_ADDRESS"]
+        del os.environ["XKNX_GENERAL_RATE_LIMIT"]
+        del os.environ["XKNX_GENERAL_MULTICAST_GROUP"]
+        del os.environ["XKNX_GENERAL_MULTICAST_PORT"]
+        self.assertEqual(str(self.xknx.own_address), self.XKNX_GENERAL_OWN_ADDRESS)
+        self.assertEqual(self.xknx.rate_limit, int(self.XKNX_GENERAL_RATE_LIMIT))
+        self.assertEqual(self.xknx.multicast_group, self.XKNX_GENERAL_MULTICAST_GROUP)
+        self.assertEqual(
+            self.xknx.multicast_port, int(self.XKNX_GENERAL_MULTICAST_PORT)
+        )
+
+    XKNX_CONNECTION_GATEWAY_IP = "192.168.12.34"
+    XKNX_CONNECTION_GATEWAY_PORT = "1234"
+    XKNX_CONNECTION_LOCAL_IP = "192.168.11.11"
+    XKNX_CONNECTION_ROUTE_BACK = "true"
+
+    def test_config_cnx_from_env(self):
+        os.environ["XKNX_CONNECTION_GATEWAY_IP"] = self.XKNX_CONNECTION_GATEWAY_IP
+        os.environ["XKNX_CONNECTION_GATEWAY_PORT"] = self.XKNX_CONNECTION_GATEWAY_PORT
+        os.environ["XKNX_CONNECTION_LOCAL_IP"] = self.XKNX_CONNECTION_LOCAL_IP
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = self.XKNX_CONNECTION_ROUTE_BACK
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_GATEWAY_IP"]
+        del os.environ["XKNX_CONNECTION_GATEWAY_PORT"]
+        del os.environ["XKNX_CONNECTION_LOCAL_IP"]
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.gateway_ip, self.XKNX_CONNECTION_GATEWAY_IP
+        )
+        self.assertEqual(
+            self.xknx.connection_config.gateway_port,
+            int(self.XKNX_CONNECTION_GATEWAY_PORT),
+        )
+        self.assertEqual(
+            self.xknx.connection_config.local_ip, self.XKNX_CONNECTION_LOCAL_IP
+        )
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            bool(self.XKNX_CONNECTION_ROUTE_BACK),
+        )
+
+    def test_config_cnx_route_back(self):
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = "true"
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            True,
+        )
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = "yes"
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            True,
+        )
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = "1"
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            True,
+        )
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = "on"
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            True,
+        )
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = "y"
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            True,
+        )
+        if "XKNX_CONNECTION_ROUTE_BACK" in os.environ:
+            del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.xknx = XKNX(config="xknx.yaml")
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            False,
+        )
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = "another_string"
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            False,
+        )
+        os.environ["XKNX_CONNECTION_ROUTE_BACK"] = ""
+        self.xknx = XKNX(config="xknx.yaml")
+        del os.environ["XKNX_CONNECTION_ROUTE_BACK"]
+        self.assertEqual(
+            self.xknx.connection_config.route_back,
+            False,
+        )
diff --git a/test/io_tests/connect_test.py b/test/io_tests/connect_test.py
index 6e61dfb9..ef52d9e2 100644
--- a/test/io_tests/connect_test.py
+++ b/test/io_tests/connect_test.py
@@ -32,7 +32,7 @@ class TestConnect(unittest.TestCase):
         """Test connecting from KNX bus."""
         xknx = XKNX()
         udp_client = UDPClient(xknx, ("192.168.1.1", 0), ("192.168.1.2", 1234))
-        connect = Connect(xknx, udp_client)
+        connect = Connect(xknx, udp_client, route_back=False)
         connect.timeout_in_seconds = 0
 
         self.assertEqual(connect.awaited_response_class, ConnectResponse)
@@ -83,3 +83,53 @@ class TestConnect(unittest.TestCase):
         self.assertTrue(connect.success)
         self.assertEqual(connect.communication_channel, 23)
         self.assertEqual(connect.identifier, 7)
+
+    def test_connect_route_back_true(self):
+        """Test connecting from KNX bus."""
+        xknx = XKNX()
+        udp_client = UDPClient(xknx, ("192.168.1.1", 0), ("192.168.1.2", 1234))
+        connect = Connect(xknx, udp_client, route_back=True)
+        connect.timeout_in_seconds = 0
+
+        self.assertEqual(connect.awaited_response_class, ConnectResponse)
+
+        # Expected KNX/IP-Frame:
+        exp_knxipframe = KNXIPFrame.init_from_body(
+            ConnectRequest(xknx, request_type=ConnectRequestType.TUNNEL_CONNECTION)
+        )
+        with patch("xknx.io.UDPClient.send") as mock_udp_send, patch(
+            "xknx.io.UDPClient.getsockname"
+        ) as mock_udp_getsockname:
+            mock_udp_getsockname.return_value = ("192.168.1.3", 4321)
+            self.loop.run_until_complete(connect.start())
+            mock_udp_send.assert_called_with(exp_knxipframe)
+
+        # Response KNX/IP-Frame with wrong type
+        wrong_knxipframe = KNXIPFrame(xknx)
+        wrong_knxipframe.init(KNXIPServiceType.CONNECTIONSTATE_REQUEST)
+        with patch("logging.Logger.warning") as mock_warning:
+            connect.response_rec_callback(wrong_knxipframe, None)
+            mock_warning.assert_called_with("Could not understand knxipframe")
+
+        # Response KNX/IP-Frame with error:
+        err_knxipframe = KNXIPFrame(xknx)
+        err_knxipframe.init(KNXIPServiceType.CONNECT_RESPONSE)
+        err_knxipframe.body.status_code = ErrorCode.E_CONNECTION_ID
+        with patch("logging.Logger.debug") as mock_warning:
+            connect.response_rec_callback(err_knxipframe, None)
+            mock_warning.assert_called_with(
+                "Error: KNX bus responded to request of type '%s' with error in '%s': %s",
+                type(connect).__name__,
+                type(err_knxipframe.body).__name__,
+                ErrorCode.E_CONNECTION_ID,
+            )
+
+        # Correct Response KNX/IP-Frame:
+        res_knxipframe = KNXIPFrame(xknx)
+        res_knxipframe.init(KNXIPServiceType.CONNECT_RESPONSE)
+        res_knxipframe.body.communication_channel = 23
+        res_knxipframe.body.identifier = 7
+        connect.response_rec_callback(res_knxipframe, None)
+        self.assertTrue(connect.success)
+        self.assertEqual(connect.communication_channel, 23)
+        self.assertEqual(connect.identifier, 7)
diff --git a/test/io_tests/connectionstate_test.py b/test/io_tests/connectionstate_test.py
index e0fc124a..c2675e1b 100644
--- a/test/io_tests/connectionstate_test.py
+++ b/test/io_tests/connectionstate_test.py
@@ -32,7 +32,9 @@ class TestConnectionState(unittest.TestCase):
         xknx = XKNX()
         communication_channel_id = 23
         udp_client = UDPClient(xknx, ("192.168.1.1", 0), ("192.168.1.2", 1234))
-        connectionstate = ConnectionState(xknx, udp_client, communication_channel_id)
+        connectionstate = ConnectionState(
+            xknx, udp_client, communication_channel_id, route_back=False
+        )
         connectionstate.timeout_in_seconds = 0
 
         self.assertEqual(
@@ -82,3 +84,60 @@ class TestConnectionState(unittest.TestCase):
         res_knxipframe.init(KNXIPServiceType.CONNECTIONSTATE_RESPONSE)
         connectionstate.response_rec_callback(res_knxipframe, None)
         self.assertTrue(connectionstate.success)
+
+    def test_connectionstate_route_back_true(self):
+        """Test connectionstateing from KNX bus."""
+        xknx = XKNX()
+        communication_channel_id = 23
+        udp_client = UDPClient(xknx, ("192.168.1.1", 0), ("192.168.1.2", 1234))
+        connectionstate = ConnectionState(
+            xknx, udp_client, communication_channel_id, route_back=True
+        )
+        connectionstate.timeout_in_seconds = 0
+
+        self.assertEqual(
+            connectionstate.awaited_response_class, ConnectionStateResponse
+        )
+        self.assertEqual(
+            connectionstate.communication_channel_id, communication_channel_id
+        )
+
+        # Expected KNX/IP-Frame:
+        exp_knxipframe = KNXIPFrame.init_from_body(
+            ConnectionStateRequest(
+                xknx,
+                communication_channel_id=communication_channel_id,
+            )
+        )
+        with patch("xknx.io.UDPClient.send") as mock_udp_send, patch(
+            "xknx.io.UDPClient.getsockname"
+        ) as mock_udp_getsockname:
+            mock_udp_getsockname.return_value = ("192.168.1.3", 4321)
+            self.loop.run_until_complete(connectionstate.start())
+            mock_udp_send.assert_called_with(exp_knxipframe)
+
+        # Response KNX/IP-Frame with wrong type
+        wrong_knxipframe = KNXIPFrame(xknx)
+        wrong_knxipframe.init(KNXIPServiceType.CONNECTIONSTATE_REQUEST)
+        with patch("logging.Logger.warning") as mock_warning:
+            connectionstate.response_rec_callback(wrong_knxipframe, None)
+            mock_warning.assert_called_with("Could not understand knxipframe")
+
+        # Response KNX/IP-Frame with error:
+        err_knxipframe = KNXIPFrame(xknx)
+        err_knxipframe.init(KNXIPServiceType.CONNECTIONSTATE_RESPONSE)
+        err_knxipframe.body.status_code = ErrorCode.E_CONNECTION_ID
+        with patch("logging.Logger.debug") as mock_warning:
+            connectionstate.response_rec_callback(err_knxipframe, None)
+            mock_warning.assert_called_with(
+                "Error: KNX bus responded to request of type '%s' with error in '%s': %s",
+                type(connectionstate).__name__,
+                type(err_knxipframe.body).__name__,
+                ErrorCode.E_CONNECTION_ID,
+            )
+
+        # Correct Response KNX/IP-Frame:
+        res_knxipframe = KNXIPFrame(xknx)
+        res_knxipframe.init(KNXIPServiceType.CONNECTIONSTATE_RESPONSE)
+        connectionstate.response_rec_callback(res_knxipframe, None)
+        self.assertTrue(connectionstate.success)
diff --git a/test/io_tests/disconnect_test.py b/test/io_tests/disconnect_test.py
index fc0a6ba6..855f15a6 100644
--- a/test/io_tests/disconnect_test.py
+++ b/test/io_tests/disconnect_test.py
@@ -32,7 +32,9 @@ class TestDisconnect(unittest.TestCase):
         xknx = XKNX()
         communication_channel_id = 23
         udp_client = UDPClient(xknx, ("192.168.1.1", 0), ("192.168.1.2", 1234))
-        disconnect = Disconnect(xknx, udp_client, communication_channel_id)
+        disconnect = Disconnect(
+            xknx, udp_client, communication_channel_id, route_back=False
+        )
         disconnect.timeout_in_seconds = 0
 
         self.assertEqual(disconnect.awaited_response_class, DisconnectResponse)
@@ -78,3 +80,56 @@ class TestDisconnect(unittest.TestCase):
         res_knxipframe.init(KNXIPServiceType.DISCONNECT_RESPONSE)
         disconnect.response_rec_callback(res_knxipframe, None)
         self.assertTrue(disconnect.success)
+
+    def test_disconnect_route_back_true(self):
+        """Test disconnecting from KNX bus."""
+        xknx = XKNX()
+        communication_channel_id = 23
+        udp_client = UDPClient(xknx, ("192.168.1.1", 0), ("192.168.1.2", 1234))
+        disconnect = Disconnect(
+            xknx, udp_client, communication_channel_id, route_back=True
+        )
+        disconnect.timeout_in_seconds = 0
+
+        self.assertEqual(disconnect.awaited_response_class, DisconnectResponse)
+        self.assertEqual(disconnect.communication_channel_id, communication_channel_id)
+
+        # Expected KNX/IP-Frame:
+        exp_knxipframe = KNXIPFrame.init_from_body(
+            DisconnectRequest(
+                xknx,
+                communication_channel_id=communication_channel_id,
+            )
+        )
+        with patch("xknx.io.UDPClient.send") as mock_udp_send, patch(
+            "xknx.io.UDPClient.getsockname"
+        ) as mock_udp_getsockname:
+            mock_udp_getsockname.return_value = ("192.168.1.3", 4321)
+            self.loop.run_until_complete(disconnect.start())
+            mock_udp_send.assert_called_with(exp_knxipframe)
+
+        # Response KNX/IP-Frame with wrong type
+        wrong_knxipframe = KNXIPFrame(xknx)
+        wrong_knxipframe.init(KNXIPServiceType.DISCONNECT_REQUEST)
+        with patch("logging.Logger.warning") as mock_warning:
+            disconnect.response_rec_callback(wrong_knxipframe, None)
+            mock_warning.assert_called_with("Could not understand knxipframe")
+
+        # Response KNX/IP-Frame with error:
+        err_knxipframe = KNXIPFrame(xknx)
+        err_knxipframe.init(KNXIPServiceType.DISCONNECT_RESPONSE)
+        err_knxipframe.body.status_code = ErrorCode.E_CONNECTION_ID
+        with patch("logging.Logger.debug") as mock_warning:
+            disconnect.response_rec_callback(err_knxipframe, None)
+            mock_warning.assert_called_with(
+                "Error: KNX bus responded to request of type '%s' with error in '%s': %s",
+                type(disconnect).__name__,
+                type(err_knxipframe.body).__name__,
+                ErrorCode.E_CONNECTION_ID,
+            )
+
+        # Correct Response KNX/IP-Frame:
+        res_knxipframe = KNXIPFrame(xknx)
+        res_knxipframe.init(KNXIPServiceType.DISCONNECT_RESPONSE)
+        disconnect.response_rec_callback(res_knxipframe, None)
+        self.assertTrue(disconnect.success)
diff --git a/test/io_tests/tunnel_test.py b/test/io_tests/tunnel_test.py
index c8f85dab..4a86deda 100644
--- a/test/io_tests/tunnel_test.py
+++ b/test/io_tests/tunnel_test.py
@@ -20,12 +20,14 @@ class TestTunnelling(unittest.TestCase):
         self.tg_received_mock = Mock()
         self.tunnel = Tunnel(
             self.xknx,
-            local_ip="192.168.1.1",
             gateway_ip="192.168.1.2",
             gateway_port=3671,
+            local_ip="192.168.1.1",
+            local_port=0,
             telegram_received_callback=self.tg_received_mock,
             auto_reconnect=False,
             auto_reconnect_wait=3,
+            route_back=False,
         )
 
     def tearDown(self):