From 9d8dfe1556c53b33721a3cf849de750e0c9e2246 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 8 May 2022 23:10:27 +0200 Subject: [PATCH] [knx] Add support for KNX IP Secure [WIP] * add support for KNX IP Secure, new options SECURETUNNEL and SECUREROUTER, refers to #8872 * add config options for credentials for secure connections * update user documentation Signed-off-by: Holger Friedrich --- bundles/org.openhab.binding.knx/README.md | 26 +++- .../knx/internal/KNXBindingConstants.java | 4 + .../internal/client/AbstractKNXClient.java | 51 ++++++- .../client/CustomKNXNetworkLinkIP.java | 1 + .../binding/knx/internal/client/IPClient.java | 132 +++++++++++++++--- .../config/IPBridgeConfiguration.java | 20 +++ .../handler/AbstractKNXThingHandler.java | 6 +- .../handler/IPBridgeThingHandler.java | 122 ++++++++++++++-- .../handler/KNXBridgeBaseThingHandler.java | 108 ++++++++++++++ .../handler/SerialBridgeThingHandler.java | 2 +- .../main/resources/OH-INF/i18n/knx.properties | 19 ++- .../src/main/resources/OH-INF/thing/ip.xml | 36 ++++- 12 files changed, 485 insertions(+), 42 deletions(-) diff --git a/bundles/org.openhab.binding.knx/README.md b/bundles/org.openhab.binding.knx/README.md index f5dd76797c538..1ef46234436f8 100644 --- a/bundles/org.openhab.binding.knx/README.md +++ b/bundles/org.openhab.binding.knx/README.md @@ -29,7 +29,7 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b | Name | Required | Description | Default value | |---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------| -| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL` or `ROUTER`) | - | +| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL`, `ROUTER`, `SECURETUNNEL` or `SECUREROUTER`) | - | | ipAddress | for `TUNNEL` | Network address of the KNX/IP gateway. If type `ROUTER` is set, the IPv4 Multicast Address can be set. | for `TUNNEL`: \, for `ROUTER`: 224.0.23.12 | | portNumber | for `TUNNEL` | Port number of the KNX/IP gateway | 3671 | | localIp | No | Network address of the local host to be used to set up the connection to the KNX/IP gateway | the system-wide configured primary interface address | @@ -204,6 +204,30 @@ Each configuration parameter has a `mainGA` where commands are written to and op The `dpt` element is optional. If omitted, the corresponding default value will be used (see the channel descriptions above). +## KNX Secure + +> NOTE: Support for KNX Secure is partly implemented for openHAB and should be considered as experimental. + +### KNX IP Secure + +KNX IP Secure protects the traffic between openHAB and your KNX installation. It **requires a KNX Secure Router or a Secure IP Interface** and a KNX installation **with security features enabled in ETS tool**. + +For *Secure routing* mode, the so called `backbone key` needs to be configured in openHAB. It is created by the ETS tool and cannot be changed via the ETS user interface. + +- The backbone key can be extracted Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `routerBackboneKey`. + +For *Secure tunneling* with a Secure IP Interface (or a router in tunneling mode), more parameters are required. A unique device authentication key, and a specific tunnel identifier and password need to be available. It can be provided to openHAB in two different ways: + +- All information can be looked up in ETS and provided separately: `tunnelDeviceAuthentication`, `tunnelUserPassword`. `tunnelUserId` is a number which is not directly visible in ETS, but can be looked up in keyring export or deduced (typically 2 for the first tunnel of a device, 3 for the second one, ...). `tunnelUserPasswort` is set in ETS in the properties of the tunnel (below the IP interface you will see the different tunnels listed) denoted as "Password". `tunnelDeviceAuthentication` is set in the properties of the IP interface itself, check for a tab "IP" and a description "Authentication Code". + +### KNX Data Secure + +KNX Data Secure protects the content of messages on the KNX bus. In a KNX installation, both classic and secure group addresses can coexist. +Data Secure does _not_ necessarily require a KNX Secure Router or a Secure IP Interface, but a KNX installation with newer KNX devices which support Data Secure and with **security features enabled in ETS tool**. + +> NOTE: **openHAB currently ignores messages with secure group addresses.** + + ## Examples The following two templates are sufficient for almost all purposes. diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java index 00dfb907fe099..1fa777fb63a2c 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java @@ -54,6 +54,10 @@ public class KNXBindingConstants { public static final String PORT_NUMBER = "portNumber"; public static final String SERIAL_PORT = "serialPort"; public static final String USE_CEMI = "useCemi"; + public static final String ROUTER_BACKBONE_GROUP_KEY = "routerBackboneGroupKey"; + public static final String TUNNEL_USER_ID = "tunnelUserId"; + public static final String TUNNEL_USER_PASSWORD = "tunnelUserPassword"; + public static final String TUNNEL_DEVICE_AUTHENTICATION = "tunnelDeviceAuthentication"; // The default multicast ip address (see iana EIBnet/IP diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java index ecc905fd68e05..6d240ae0447fe 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java @@ -55,6 +55,7 @@ import tuwien.auto.calimero.process.ProcessCommunicatorImpl; import tuwien.auto.calimero.process.ProcessEvent; import tuwien.auto.calimero.process.ProcessListener; +import tuwien.auto.calimero.secure.KnxSecureException; import tuwien.auto.calimero.secure.SecureApplicationLayer; import tuwien.auto.calimero.secure.Security; @@ -92,6 +93,9 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien private final Set groupAddressListeners = new CopyOnWriteArraySet<>(); private final LinkedBlockingQueue readDatapoints = new LinkedBlockingQueue<>(); + private boolean firstConnect; + private long lastDisconnectSysMillis; + @FunctionalInterface private interface ListenerNotification { void apply(BusMessageListener listener, IndividualAddress source, GroupAddress destination, byte[] asdu); @@ -102,6 +106,7 @@ private interface ListenerNotification { @Override public void detached(DetachEvent e) { + lastDisconnectSysMillis = System.currentTimeMillis(); logger.debug("The KNX network link was detached from the process communicator"); } @@ -136,6 +141,8 @@ public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int respons this.readRetriesLimit = readRetriesLimit; this.knxScheduler = knxScheduler; this.statusUpdateCallback = statusUpdateCallback; + firstConnect = true; + lastDisconnectSysMillis = System.currentTimeMillis(); } public void initialize() { @@ -146,7 +153,9 @@ public void initialize() { private boolean scheduleReconnectJob() { if (autoReconnectPeriod > 0) { - connectJob = knxScheduler.schedule(this::connect, autoReconnectPeriod, TimeUnit.SECONDS); + // schedule connect job, for the first connection ignore autoReconnectPeriod and use 1 sec + connectJob = knxScheduler.schedule(this::connect, firstConnect ? 1 : autoReconnectPeriod, TimeUnit.SECONDS); + firstConnect = false; return true; } else { return false; @@ -174,40 +183,69 @@ private synchronized boolean connect() { if (isConnected()) { return true; } + long now = System.currentTimeMillis(); + if ((now - lastDisconnectSysMillis) < 1000) { + logger.debug("fast reconnect"); + // SECURETUNNEL: TCP Connections need additional efforts to avoid grabbing all tunnel connections + // the interface, this is implemented in IPClient + } try { + // we have a valid "connection" object, this is ensured by IPClient.java + + // this is a bit misleading, it is actually removing all registered users of this connection releaseConnection(); logger.debug("Bridge {} is connecting to the KNX bus", thingUID); + // now establish (possibly encrypted) connection, according to settings (tunnel, routing, secure...) KNXNetworkLink link = establishConnection(); this.link = link; + // ManagementProcedures provided by Calimero: allow managing other KNX devices, e.g. check if an address is + // reachable. + // Note for KNX Secure: ManagmentProcedueresImpl currently does not provide a ctor with external SAL, + // it internally creates an instance of ManagementClientImpl, which uses + // Security.defaultInstallation().deviceToolKeys() + // Protected ctor using given ManagementClientImpl is avalable (custom class to be inherited) managementProcedures = new ManagementProceduresImpl(link); + // ManagementClient provided by Calimero: allow reading device info, etc. + // Note for KNX Secure: ManagementClientImpl does not provide a ctor with external SAL in Calimero 2.5, + // is uses global Security.defaultInstalltion().deviceToolKeys() + // Current main branch includes a protected ctor (custom class to be inherited) + // TODO Calimero>2.5: check if there is a new way to provide security info, there is a new protected ctor + // TODO check if we can avoid creating another ManagementClient and re-use this from ManagemntProcedures ManagementClient managementClient = new ManagementClientImpl(link); managementClient.responseTimeout(Duration.ofSeconds(responseTimeout)); this.managementClient = managementClient; + // OH helper for reading device info, based on managementClient above deviceInfoClient = new DeviceInfoClientImpl(managementClient); + // ProcessCommunicator provides main KNX communication (Calimero). + // Note for KNX Secure: SAL to be provided ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link); processCommunicator.responseTimeout(Duration.ofSeconds(responseTimeout)); processCommunicator.addProcessListener(processListener); this.processCommunicator = processCommunicator; + // ProcessCommunicationResponder provides responses to requests from KNX bus (Calimero). + // Note for KNX Secure: SAL to be provided ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link, new SecureApplicationLayer(link, Security.defaultInstallation())); this.responseCommunicator = responseCommunicator; + // register this class, callbacks will be triggered link.addLinkListener(this); + // create a job carrying out read requests busJob = knxScheduler.scheduleWithFixedDelay(() -> readNextQueuedDatapoint(), 0, readingPause, TimeUnit.MILLISECONDS); statusUpdateCallback.updateStatus(ThingStatus.ONLINE); connectJob = null; return true; - } catch (KNXException | InterruptedException e) { + } catch (KNXException | InterruptedException | KnxSecureException e) { logger.debug("Error connecting to the bus: {}", e.getMessage(), e); disconnect(e); scheduleReconnectJob(); @@ -216,9 +254,11 @@ private synchronized boolean connect() { } private void disconnect(@Nullable Exception e) { + lastDisconnectSysMillis = System.currentTimeMillis(); + releaseConnection(); if (e != null) { - String message = e.getLocalizedMessage(); + final String message = e.getLocalizedMessage(); statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message != null ? message : ""); } else { @@ -227,14 +267,13 @@ private void disconnect(@Nullable Exception e) { } @SuppressWarnings("null") - private void releaseConnection() { + protected void releaseConnection() { logger.debug("Bridge {} is disconnecting from the KNX bus", thingUID); readDatapoints.clear(); busJob = nullify(busJob, j -> j.cancel(true)); deviceInfoClient = null; managementProcedures = nullify(managementProcedures, mp -> mp.detach()); managementClient = nullify(managementClient, mc -> mc.detach()); - link = nullify(link, l -> l.close()); processCommunicator = nullify(processCommunicator, pc -> { pc.removeProcessListener(processListener); pc.detach(); @@ -243,6 +282,7 @@ private void releaseConnection() { rc.removeProcessListener(processListener); rc.detach(); }); + link = nullify(link, l -> l.close()); } private @Nullable T nullify(T target, @Nullable Consumer lastWill) { @@ -276,6 +316,7 @@ private String toDPTValue(Type type, String dpt) { return typeHelper.toDPTValue(type, dpt); } + // datapoint is null at end of the list, warning is misleading @SuppressWarnings("null") private void readNextQueuedDatapoint() { if (!connectIfNotAutomatic()) { diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java index 266b5231f2aa3..350cce8c1929e 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java @@ -30,6 +30,7 @@ public class CustomKNXNetworkLinkIP extends KNXNetworkLinkIP { public static final int TUNNELING = KNXNetworkLinkIP.TUNNELING; + public static final int TUNNELINGV2 = KNXNetworkLinkIP.TunnelingV2; public static final int ROUTING = KNXNetworkLinkIP.ROUTING; CustomKNXNetworkLinkIP(final int serviceMode, KNXnetIPConnection conn, KNXMediumSettings settings) diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java index 045fefadb63fe..ea03272b3987f 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java @@ -17,6 +17,7 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; +import java.time.Duration; import java.util.concurrent.ScheduledExecutorService; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -32,6 +33,9 @@ import tuwien.auto.calimero.knxnetip.KNXnetIPRouting; import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel; import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel.TunnelingLayer; +import tuwien.auto.calimero.knxnetip.SecureConnection; +import tuwien.auto.calimero.knxnetip.TcpConnection; +import tuwien.auto.calimero.knxnetip.TcpConnection.SecureSession; import tuwien.auto.calimero.link.KNXNetworkLink; import tuwien.auto.calimero.link.KNXNetworkLinkIP; import tuwien.auto.calimero.link.medium.KNXMediumSettings; @@ -46,23 +50,42 @@ @NonNullByDefault public class IPClient extends AbstractKNXClient { + public enum IpConnectionType { + TUNNEL, + ROUTER, + SECURE_TUNNEL, + SECURE_ROUTER + }; + private final Logger logger = LoggerFactory.getLogger(IPClient.class); private static final String MODE_ROUTER = "ROUTER"; private static final String MODE_TUNNEL = "TUNNEL"; + private static final String MODE_SECURE_ROUTER = "SECURE ROUTER"; + private static final String MODE_SECURE_TUNNEL = "SECURE TUNNEL"; + private static final long PAUSE_ON_TCP_SESSION_CLOSE_MS = 1000; - private final int ipConnectionType; + private final IpConnectionType ipConnectionType; private final String ip; private final String localSource; private final int port; @Nullable private final InetSocketAddress localEndPoint; private final boolean useNAT; + private final byte[] secureRoutingBackboneGroupKey; + private final long secureRoutingLatencyToleranceMs; + private final byte[] secureTunnelDevKey; + private final int secureTunnelUser; + private final byte[] secureTunnelUserKey; + + @Nullable + SecureSession tcpSession; - public IPClient(int ipConnectionType, String ip, String localSource, int port, - @Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod, ThingUID thingUID, - int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler, - StatusUpdateCallback statusUpdateCallback) { + public IPClient(IpConnectionType ipConnectionType, String ip, String localSource, int port, + @Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod, + byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey, + int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause, + int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) { super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler, statusUpdateCallback); this.ipConnectionType = ipConnectionType; @@ -71,34 +94,61 @@ public IPClient(int ipConnectionType, String ip, String localSource, int port, this.port = port; this.localEndPoint = localEndPoint; this.useNAT = useNAT; + this.secureRoutingBackboneGroupKey = secureRoutingBackboneGroupKey; + this.secureRoutingLatencyToleranceMs = secureRoutingLatencyToleranceMs; + this.secureTunnelDevKey = secureTunnelDevKey; + this.secureTunnelUser = secureTunnelUser; + this.secureTunnelUserKey = secureTunnelUserKey; + tcpSession = null; } @Override protected KNXNetworkLink establishConnection() throws KNXException, InterruptedException { - logger.debug("Establishing connection to KNX bus on {}:{} in mode {}.", ip, port, connectionTypeToString()); + logger.info("Establishing connection to KNX bus on {}:{} in mode {}.", ip, port, connectionTypeToString()); TPSettings settings = new TPSettings(new IndividualAddress(localSource)); return createKNXNetworkLinkIP(ipConnectionType, localEndPoint, new InetSocketAddress(ip, port), useNAT, settings); } private String connectionTypeToString() { - return ipConnectionType == CustomKNXNetworkLinkIP.ROUTING ? MODE_ROUTER : MODE_TUNNEL; + if (ipConnectionType == IpConnectionType.ROUTER) { + return MODE_ROUTER; + } + if (ipConnectionType == IpConnectionType.TUNNEL) { + return MODE_TUNNEL; + } + if (ipConnectionType == IpConnectionType.SECURE_ROUTER) { + return MODE_SECURE_ROUTER; + } + if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) { + return MODE_SECURE_TUNNEL; + } + return "unknown connection type"; } - private KNXNetworkLinkIP createKNXNetworkLinkIP(int serviceMode, @Nullable InetSocketAddress localEP, - @Nullable InetSocketAddress remoteEP, boolean useNAT, KNXMediumSettings settings) - throws KNXException, InterruptedException { + private KNXNetworkLinkIP createKNXNetworkLinkIP(IpConnectionType ipConnectionType, + @Nullable InetSocketAddress localEP, @Nullable InetSocketAddress remoteEP, boolean useNAT, + KNXMediumSettings settings) throws KNXException, InterruptedException { + // Calimero service mode, ROUTING for both classic and secure routing + int serviceMode = CustomKNXNetworkLinkIP.ROUTING; + if (ipConnectionType == IpConnectionType.TUNNEL) { + serviceMode = CustomKNXNetworkLinkIP.TUNNELING; + } else if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) { + serviceMode = CustomKNXNetworkLinkIP.TUNNELINGV2; + } + // creating the connection here as a workaround for // https://github.com/calimero-project/calimero-core/issues/57 - KNXnetIPConnection conn = getConnection(serviceMode, localEP, remoteEP, useNAT); + KNXnetIPConnection conn = getConnection(ipConnectionType, localEP, remoteEP, useNAT); return new CustomKNXNetworkLinkIP(serviceMode, conn, settings); } - private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAddress localEP, + private KNXnetIPConnection getConnection(IpConnectionType ipConnectionType, @Nullable InetSocketAddress localEP, @Nullable InetSocketAddress remoteEP, boolean useNAT) throws KNXException, InterruptedException { KNXnetIPConnection conn; - switch (serviceMode) { - case CustomKNXNetworkLinkIP.TUNNELING: + switch (ipConnectionType) { + case TUNNEL: + case SECURE_TUNNEL: InetSocketAddress local = localEP; if (local == null) { try { @@ -107,9 +157,29 @@ private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAd throw new KNXException("no local host available"); } } - conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT); + if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) { + // the follwing would create a secure conn via UDP, default should be TCP + // conn = SecureConnection.newTunneling(TunnelingLayer.LinkLayer, local, remoteEP, useNAT, + // secureTunnelDevKey, secureTunnelUser, secureTunnelUserKey); + + // TODO: possibly implement TCP connection management + logger.debug("creating new TCP connection"); + if (tcpSession != null) { + logger.warn("tcpSession might still be open"); + } + // using .clone for the keys is essential - otherwise Calimero clears the array and a reconnect will + // fail + tcpSession = TcpConnection.newTcpConnection(localEP, remoteEP).newSecureSession(secureTunnelUser, + secureTunnelUserKey.clone(), secureTunnelDevKey.clone()); + logger.debug("creating new tunnel"); + conn = SecureConnection.newTunneling(TunnelingLayer.LinkLayer, tcpSession, + new IndividualAddress(localSource)); + } else { + conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT); + } break; - case CustomKNXNetworkLinkIP.ROUTING: + case ROUTER: + case SECURE_ROUTER: NetworkInterface netIf = null; if (localEP != null && !localEP.isUnresolved()) { try { @@ -119,11 +189,39 @@ private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAd } } final InetAddress mcast = remoteEP != null ? remoteEP.getAddress() : null; - conn = new KNXnetIPRouting(netIf, mcast); + if (ipConnectionType == IpConnectionType.SECURE_ROUTER) { + conn = SecureConnection.newRouting(netIf, mcast, secureRoutingBackboneGroupKey, + Duration.ofMillis(secureRoutingLatencyToleranceMs)); + } else { + conn = new KNXnetIPRouting(netIf, mcast); + } break; default: throw new KNXIllegalArgumentException("unknown service mode"); } return conn; } + + private void closeTcpConnection() { + final SecureSession toBeClosed = tcpSession; + if (toBeClosed != null) { + tcpSession = null; + logger.debug("force close TCP connection"); + try { + toBeClosed.close(); + try { + Thread.sleep(PAUSE_ON_TCP_SESSION_CLOSE_MS); + } catch (InterruptedException e) { + } + } catch (Exception e) { + logger.warn("closing TCP connection failed: {}", e.getMessage()); + } + } + } + + @Override + protected void releaseConnection() { + closeTcpConnection(); + super.releaseConnection(); + } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java index 6c6b8816aab71..f6c77e038da10 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java @@ -31,6 +31,10 @@ public class IPBridgeConfiguration extends BridgeConfiguration { private BigDecimal portNumber = BigDecimal.valueOf(0); private String localIp = ""; private String localSourceAddr = ""; + private String routerBackboneKey = ""; + private String tunnelUserId = ""; + private String tunnelUserPassword = ""; + private String tunnelDeviceAuthentication = ""; public Boolean getUseNAT() { return useNAT; @@ -55,4 +59,20 @@ public String getLocalIp() { public String getLocalSourceAddr() { return localSourceAddr; } + + public String getRouterBackboneKey() { + return routerBackboneKey; + } + + public String getTunnelUserId() { + return tunnelUserId; + } + + public String getTunnelUserPassword() { + return tunnelUserPassword; + } + + public String getTunnelDeviceAuthentication() { + return tunnelDeviceAuthentication; + } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java index 318f58f14945a..27dcef9e42db7 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java @@ -162,7 +162,8 @@ private void pollDeviceStatus() { } } } catch (KNXException e) { - logger.debug("An error occurred while testing the reachability of a thing '{}'", getThing().getUID(), e); + logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(), + e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage()); } } @@ -191,7 +192,8 @@ protected void attachToClient() { updateStatus(ThingStatus.ONLINE); } } catch (KNXFormatException e) { - logger.debug("An exception occurred while setting the individual address '{}'", config.getAddress(), e); + logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(), + e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage()); } getClient().registerGroupAddressListener(this); diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java index ad935d9e161c9..149858934fa73 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java @@ -14,11 +14,11 @@ import java.net.InetSocketAddress; import java.text.MessageFormat; +import java.util.concurrent.Future; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.KNXBindingConstants; -import org.openhab.binding.knx.internal.client.CustomKNXNetworkLinkIP; import org.openhab.binding.knx.internal.client.IPClient; import org.openhab.binding.knx.internal.client.KNXClient; import org.openhab.binding.knx.internal.client.NoOpClient; @@ -43,6 +43,9 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler { private static final String MODE_ROUTER = "ROUTER"; private static final String MODE_TUNNEL = "TUNNEL"; + private static final String MODE_SECURE_ROUTER = "SECUREROUTER"; + private static final String MODE_SECURE_TUNNEL = "SECURETUNNEL"; + private @Nullable Future initJob = null; private final Logger logger = LoggerFactory.getLogger(IPBridgeThingHandler.class); @@ -56,7 +59,48 @@ public IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressS @Override public void initialize() { + // initialisation would take too long and show a warning during binding startup + // KNX secure is adding serious delay + initJob = scheduler.submit(() -> { + initializeLater(); + }); + } + + public void initializeLater() { + logger.trace("KNX scheduled initialization started"); + + dispose(); + IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class); + boolean securityAvailable = false; + try { + securityAvailable = initializeSecurity(config.getRouterBackboneKey(), + config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword()); + if (securityAvailable) { + logger.info("KNX secure: router backboneGroupKey is {} set", + ((secureRouting.backboneGroupKey.length == 16) ? "properly" : "not")); + boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16) + && (secureTunnel.userKey.length == 16)); + logger.info("KNX secure: tunnel keys are {} set", (tunnelOk ? "properly" : "not")); + } else { + logger.debug("KNX security not configured"); + } + } catch (Exception e) { + // KnxSecureException or others + if (e instanceof NullPointerException) { + logger.warn("initialization failed", e); // for NPE log a stack trace as well + } else { + logger.warn("{}", e.toString()); + } + + String message = e.getLocalizedMessage(); + if (message == null) { + message = e.getClass().getSimpleName(); + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "KNX security: " + message); + return; + } + int autoReconnectPeriod = config.getAutoReconnectPeriod(); if (autoReconnectPeriod != 0 && autoReconnectPeriod < 30) { logger.info("autoReconnectPeriod for {} set to {}s, allowed range is 0 (never) or >30", thing.getUID(), @@ -70,20 +114,59 @@ public void initialize() { String ip = config.getIpAddress(); InetSocketAddress localEndPoint = null; boolean useNAT = false; - int ipConnectionType; + + IPClient.IpConnectionType ipConnectionType; if (MODE_TUNNEL.equalsIgnoreCase(connectionTypeString)) { useNAT = config.getUseNAT(); - ipConnectionType = CustomKNXNetworkLinkIP.TUNNELING; + ipConnectionType = IPClient.IpConnectionType.TUNNEL; + } else if (MODE_SECURE_TUNNEL.equalsIgnoreCase(connectionTypeString)) { + useNAT = config.getUseNAT(); + ipConnectionType = IPClient.IpConnectionType.SECURE_TUNNEL; + + if (!securityAvailable) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Security configuration missing for secure tunnel"); + return; + } + boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16) + && (secureTunnel.userKey.length == 16)); + if (!tunnelOk) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Security configuration for secure tunnel is incomplete"); + return; + } + + logger.info("KNX secure tunneling needs a few seconds to establish connection"); + // user id, key, devAuth are already stored } else if (MODE_ROUTER.equalsIgnoreCase(connectionTypeString)) { useNAT = false; if (ip.isEmpty()) { ip = KNXBindingConstants.DEFAULT_MULTICAST_IP; } - ipConnectionType = CustomKNXNetworkLinkIP.ROUTING; + ipConnectionType = IPClient.IpConnectionType.ROUTER; + } else if (MODE_SECURE_ROUTER.equalsIgnoreCase(connectionTypeString)) { + useNAT = false; + if (ip.isEmpty()) { + ip = KNXBindingConstants.DEFAULT_MULTICAST_IP; + } + ipConnectionType = IPClient.IpConnectionType.SECURE_ROUTER; + + if (!securityAvailable) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Security configuration missing for secure routing"); + return; + } + if (secureRouting.backboneGroupKey.length != 16) { + // failed to read shared backbone group key from config + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "backboneGroupKey required for secure routing; please configure"); + return; + } + logger.info("KNX secure routing needs a few seconds to establish connection"); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - MessageFormat.format("Unknown IP connection type {0}. Known types are either 'TUNNEL' or 'ROUTER'", - connectionTypeString)); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MessageFormat.format( + "Unknown IP connection type {0}. Known types are either 'TUNNEL', 'ROUTER', 'SECURETUNNEL', or 'SECUREROUTER'", + connectionTypeString)); return; } @@ -95,23 +178,34 @@ public void initialize() { updateStatus(ThingStatus.UNKNOWN); client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod, - thing.getUID(), config.getResponseTimeout().intValue(), config.getReadingPause().intValue(), - config.getReadRetriesLimit().intValue(), getScheduler(), this); + secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey, + secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout().intValue(), + config.getReadingPause().intValue(), config.getReadRetriesLimit().intValue(), getScheduler(), this); final var tmpClient = client; if (tmpClient != null) { tmpClient.initialize(); } + + logger.trace("KNX scheduled initialization completed"); } @Override public void dispose() { - super.dispose(); - final var tmpClient = client; - if (tmpClient != null) { - tmpClient.dispose(); - client = null; + { + final var tmpInitJob = initJob; + if (tmpInitJob != null) { + tmpInitJob.cancel(true); + } + } + { + final var tmpClient = client; + if (tmpClient != null) { + tmpClient.dispose(); + client = null; + } } + super.dispose(); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java index e9c087b4acf5a..1ed7be6dbd9b8 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java @@ -29,27 +29,135 @@ import org.openhab.core.types.Command; import tuwien.auto.calimero.IndividualAddress; +import tuwien.auto.calimero.knxnetip.SecureConnection; import tuwien.auto.calimero.mgmt.Destination; +import tuwien.auto.calimero.secure.KnxSecureException; /** * The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Simon Kaufmann - Initial contribution and API + * @author Holger Friedrich - KNX Secure configuration */ @NonNullByDefault public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implements StatusUpdateCallback { + public static class SecureTunnelConfig { + public SecureTunnelConfig() { + devKey = new byte[0]; + userKey = new byte[0]; + user = 0; + } + + public byte[] devKey; + public byte[] userKey; + public int user = 0; + } + + public static class SecureRoutingConfig { + public SecureRoutingConfig() { + backboneGroupKey = new byte[0]; + latencyToleranceMs = 0; + } + + public byte[] backboneGroupKey; + public long latencyToleranceMs = 0; + } + protected ConcurrentHashMap destinations = new ConcurrentHashMap<>(); private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx"); private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor(); + protected SecureRoutingConfig secureRouting; + protected SecureTunnelConfig secureTunnel; public KNXBridgeBaseThingHandler(Bridge bridge) { super(bridge); + secureRouting = new SecureRoutingConfig(); + secureTunnel = new SecureTunnelConfig(); } protected abstract KNXClient getClient(); + /*** + * Initialize KNX secure if configured (full interface) + * + * @param cRouterBackboneGroupKey shared key for secure router mode. + * @param cTunnelDevAuth device password for IP interface in tunnel mode. + * @param cTunnelUser user id for tunnel mode. Must be an integer >0. + * @param cTunnelPassword user password for tunnel mode. + * @return + */ + protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTunnelDevAuth, String cTunnelUser, + String cTunnelPassword) { + secureRouting = new SecureRoutingConfig(); + secureTunnel = new SecureTunnelConfig(); + + boolean securityInitialized = false; + + // step 1: secure routing, backbone group key manually specified in OH config + if (!cRouterBackboneGroupKey.isBlank()) { + // provided in conig + String tmp = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", ""); + if (!tmp.isEmpty()) { + // helper may throw KnxSecureException + secureRouting.backboneGroupKey = secHelperParseBackboneKey(tmp); + securityInitialized = true; + } + } + + // step 2: check if valid tunnel parameters are specified in config + if (!cTunnelDevAuth.isBlank()) { + secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray()); + securityInitialized = true; + } + if (!cTunnelPassword.isBlank()) { + secureTunnel.userKey = SecureConnection.hashUserPassword(cTunnelPassword.toCharArray()); + securityInitialized = true; + } + if (!cTunnelUser.isBlank()) { + String user = cTunnelUser.trim(); + try { + secureTunnel.user = Integer.decode(user); + } catch (NumberFormatException e) { + throw new KnxSecureException("tunnelUser must be a number >0"); + } + if (secureTunnel.user <= 0) { + throw new KnxSecureException("tunnelUser must be a number >0"); + } + securityInitialized = true; + } + + // step 5: router: load latencyTolerance + // default to 2000ms + // this parameter is currently not exposed in config, it may later be set by using the keyring + secureRouting.latencyToleranceMs = 2000; + + return securityInitialized; + } + + /*** + * converts hex string (32 characters) to byte[16] + * + * @param hexstring 32 characters hex + * @return key in byte array format + */ + public static byte[] secHelperParseBackboneKey(String hexstring) throws NumberFormatException, KnxSecureException { + if (hexstring.length() != 32) { + throw new KnxSecureException("backbone key must be 32 characters (16 byte hex notation)"); + } + + byte[] parsed = new byte[16]; + try { + for (byte i = 0; i < 16; i++) { + parsed[i] = (byte) Integer.parseInt(hexstring.substring(2 * i, 2 * i + 2), 16); + } + } catch (NumberFormatException e) { + throw new KnxSecureException("backbone key configured, cannot parse hex string, illegal character"); + } + return parsed; + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { // Nothing to do here diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java index 4ed6ab511371e..102332033a53e 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java @@ -50,8 +50,8 @@ public void initialize() { @Override public void dispose() { - super.dispose(); client.dispose(); + super.dispose(); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties index 092aa7b582af3..845255ac2d4a0 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties @@ -24,6 +24,8 @@ thing-type.config.knx.device.readInterval.label = Interval thing-type.config.knx.device.readInterval.description = Interval (in seconds) between attempts to read the status group addresses on the bus thing-type.config.knx.ip.autoReconnectPeriod.label = Auto Reconnect Period thing-type.config.knx.ip.autoReconnectPeriod.description = Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s +thing-type.config.knx.ip.group.knxsecure.label = KNX secure +thing-type.config.knx.ip.group.knxsecure.description = Settings for KNX secure. Requires KNX secure features to be active in KNX installation. thing-type.config.knx.ip.ipAddress.label = Network Address thing-type.config.knx.ip.ipAddress.description = Network address of the KNX/IP gateway thing-type.config.knx.ip.localIp.label = Local Network Address @@ -38,10 +40,20 @@ thing-type.config.knx.ip.readingPause.label = Reading Pause thing-type.config.knx.ip.readingPause.description = Time in milliseconds of how long should be paused between two read requests to the bus during initialization thing-type.config.knx.ip.responseTimeout.label = Response Timeout thing-type.config.knx.ip.responseTimeout.description = Seconds to wait for a response from the KNX bus +thing-type.config.knx.ip.routerBackboneKey.label = Router backbone key +thing-type.config.knx.ip.routerBackboneKey.description = KNX secure, optional: Backbone key for secure router mode. 16 bytes in hex notation. Can also be found in ETS security report. +thing-type.config.knx.ip.tunnelDeviceAuthentication.label = Tunnel device authentication +thing-type.config.knx.ip.tunnelDeviceAuthentication.description = KNX secure, optional: Tunnel device authentication for secure tunnel mode. +thing-type.config.knx.ip.tunnelUserId.label = Tunnel user id +thing-type.config.knx.ip.tunnelUserId.description = KNX secure, optional: Tunnel user id for secure tunnel mode. +thing-type.config.knx.ip.tunnelUserPassword.label = Tunnel user password +thing-type.config.knx.ip.tunnelUserPassword.description = KNX secure, optional: Tunnel user key for secure tunnel mode. thing-type.config.knx.ip.type.label = IP Connection Type -thing-type.config.knx.ip.type.description = The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER +thing-type.config.knx.ip.type.description = The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or SECUREROUTER thing-type.config.knx.ip.type.option.TUNNEL = Tunnel thing-type.config.knx.ip.type.option.ROUTER = Router +thing-type.config.knx.ip.type.option.SECURETUNNEL = Secure tunnel (experimental) +thing-type.config.knx.ip.type.option.SECUREROUTER = Secure router (experimental) thing-type.config.knx.ip.useNAT.label = Use NAT thing-type.config.knx.ip.useNAT.description = Set to "true" when having network address translation between this server and the gateway thing-type.config.knx.serial.autoReconnectPeriod.label = Auto Reconnect Period @@ -134,3 +146,8 @@ channel-type.config.knx.rollershutter.upDown.label = Address channel-type.config.knx.rollershutter.upDown.description = The group address(es) in Group Address Notation to move the shutter in the DOWN or UP direction channel-type.config.knx.single.ga.label = Address channel-type.config.knx.single.ga.description = The group address(es) in Group Address Notation + +# thing types config + +thing-type.config.knx.serial.group.knxsecure.label = KNX secure +thing-type.config.knx.serial.group.knxsecure.description = Settings for KNX secure. Requires KNX secure features to be active in KNX installation. diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml index 89e2c6472f22f..35c9d4e89b746 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml @@ -9,12 +9,20 @@ This is a KNX IP interface or router + + + Settings for KNX secure. Requires KNX secure features to be active in KNX installation. + true + - The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER + The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or + SECUREROUTER + + @@ -64,6 +72,32 @@ Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s 60 + + password + + KNX secure, optional: Backbone key for secure router mode. 16 bytes + in hex notation. Can also be found + in ETS security report. + + + + + KNX secure, optional: Tunnel user id for secure tunnel mode. + + + + password + + KNX secure, optional: Tunnel user key for secure tunnel mode. + + + + password + + KNX secure, optional: Tunnel device authentication for secure tunnel mode. + +