| 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`: \<nothing\>, 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 |
| responseTimeout | No | Timeout in seconds to wait for a response from the KNX bus | 10 |
| readRetriesLimit | No | Limits the read retries while initialization from the KNX bus | 3 |
| autoReconnectPeriod | No | Seconds between connect retries when KNX link has been lost (0 means never). | 0 |
+| routerBackboneKey | No | KNX secure: Backbone key for secure router mode | - |
+| tunnelUserId | No | KNX secure: Tunnel user id for secure tunnel mode (if specified, it must be a number >0) | - |
+| tunnelUserPassword | No | KNX secure: Tunnel user key for secure tunnel mode | - |
+| tunnelDeviceAuthentication | No | KNX secure: Tunnel device authentication for secure tunnel mode | - |
### Serial Gateway
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 from 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.
+
+- 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.
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 <a
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP
package org.openhab.binding.knx.internal.client;
import java.time.Duration;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CopyOnWriteArraySet;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXException;
+import tuwien.auto.calimero.KNXIllegalArgumentException;
import tuwien.auto.calimero.datapoint.CommandDP;
import tuwien.auto.calimero.datapoint.Datapoint;
import tuwien.auto.calimero.device.ProcessCommunicationResponder;
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;
*/
@NonNullByDefault
public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClient {
+ public enum ClientState {
+ INIT,
+ RUNNING,
+ INTERRUPTED,
+ DISPOSE
+ }
+
+ private ClientState state = ClientState.INIT;
private static final int MAX_SEND_ATTEMPTS = 2;
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
+ final long reconnectDelayS = (state == ClientState.INIT) ? 1 : autoReconnectPeriod;
+ final String prefix = (state == ClientState.INIT) ? "re" : "";
+ logger.debug("Bridge {} scheduling {}connect in {}s", thingUID, prefix, reconnectDelayS);
+ connectJob = knxScheduler.schedule(this::connect, reconnectDelayS, TimeUnit.SECONDS);
return true;
} else {
return false;
}
private void cancelReconnectJob() {
- ScheduledFuture<?> currentReconnectJob = connectJob;
+ final ScheduledFuture<?> currentReconnectJob = connectJob;
if (currentReconnectJob != null) {
currentReconnectJob.cancel(true);
connectJob = null;
}
private synchronized boolean connect() {
+ if (state == ClientState.INIT) {
+ state = ClientState.RUNNING;
+ } else if (state == ClientState.DISPOSE) {
+ logger.trace("connect() ignored, closing down");
+ return false;
+ }
+
if (isConnected()) {
return true;
}
try {
+ // We have a valid "connection" object, this is ensured by IPClient.java.
+ // "releaseConnection" is actually removing all registered users of this connection and stopping
+ // all threads.
+ // Note that this will also kill this function in the following call to sleep in case of a
+ // connection loss -> restart is via triggered via scheduledReconnect in handler for InterruptedException.
releaseConnection();
+ Thread.sleep(1000);
+ logger.debug("Bridge {} is connecting to KNX bus", thingUID);
- 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;
+
+ logger.info("Bridge {} connected to KNX bus", thingUID);
+
+ state = ClientState.RUNNING;
return true;
- } catch (KNXException | InterruptedException e) {
- logger.debug("Error connecting to the bus: {}", e.getMessage(), e);
+ } catch (InterruptedException e) {
+ final var lastState = state;
+ state = ClientState.INTERRUPTED;
+
+ logger.trace("Bridge {}, connection interrupted", thingUID);
+
+ disconnect(e);
+ if (lastState != ClientState.DISPOSE) {
+ scheduleReconnectJob();
+ }
+
+ return false;
+ } catch (KNXException | KnxSecureException e) {
+ logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
disconnect(e);
scheduleReconnectJob();
return false;
+ } catch (KNXIllegalArgumentException e) {
+ logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
+ disconnect(e, Optional.of(ThingStatusDetail.CONFIGURATION_ERROR));
+ return false;
}
}
private void disconnect(@Nullable Exception e) {
+ disconnect(e, Optional.empty());
+ }
+
+ private synchronized void disconnect(@Nullable Exception e, Optional<ThingStatusDetail> detail) {
releaseConnection();
if (e != null) {
- String message = e.getLocalizedMessage();
- statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ final String message = e.getLocalizedMessage();
+ statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, detail.orElse(ThingStatusDetail.COMMUNICATION_ERROR),
message != null ? message : "");
} else {
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
}
@SuppressWarnings("null")
- private void releaseConnection() {
- logger.debug("Bridge {} is disconnecting from the KNX bus", thingUID);
- readDatapoints.clear();
+ protected void releaseConnection() {
+ logger.debug("Bridge {} is disconnecting from KNX bus", thingUID);
+ var tmplink = link;
+ if (tmplink != null) {
+ link.removeLinkListener(this);
+ }
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();
- });
+ readDatapoints.clear();
responseCommunicator = nullify(responseCommunicator, rc -> {
rc.removeProcessListener(processListener);
rc.detach();
});
+ processCommunicator = nullify(processCommunicator, pc -> {
+ pc.removeProcessListener(processListener);
+ pc.detach();
+ });
+ deviceInfoClient = null;
+ managementClient = nullify(managementClient, mc -> mc.detach());
+ managementProcedures = nullify(managementProcedures, mp -> mp.detach());
+ link = nullify(link, l -> l.close());
+ logger.trace("Bridge {} disconnected from KNX bus", thingUID);
}
private <T> @Nullable T nullify(T target, @Nullable Consumer<T> lastWill) {
return typeHelper.toDPTValue(type, dpt);
}
+ // datapoint is null at end of the list, warning is misleading
@SuppressWarnings("null")
private void readNextQueuedDatapoint() {
if (!connectIfNotAutomatic()) {
}
public void dispose() {
+ state = ClientState.DISPOSE;
+
cancelReconnectJob();
disconnect(null);
}
ProcessCommunicator processCommunicator = this.processCommunicator;
KNXNetworkLink link = this.link;
if (processCommunicator == null || link == null) {
- logger.debug("Cannot write to the KNX bus (processCommuicator: {}, link: {})",
+ logger.debug("Cannot write to KNX bus (processCommuicator: {}, link: {})",
processCommunicator == null ? "Not OK" : "OK",
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
return;
ProcessCommunicationResponder responseCommunicator = this.responseCommunicator;
KNXNetworkLink link = this.link;
if (responseCommunicator == null || link == null) {
- logger.debug("Cannot write to the KNX bus (responseCommunicator: {}, link: {})",
+ logger.debug("Cannot write to KNX bus (responseCommunicator: {}, link: {})",
responseCommunicator == null ? "Not OK" : "OK",
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
return;
break;
} catch (KNXException e) {
if (i < MAX_SEND_ATTEMPTS - 1) {
- logger.debug("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Will retry.",
- type, datapoint, e.getLocalizedMessage());
+ logger.debug("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Will retry.", type,
+ datapoint, e.getLocalizedMessage());
} else {
- logger.warn("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Giving up now.",
+ logger.warn("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Giving up now.",
type, datapoint, e.getLocalizedMessage());
throw e;
}
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)
return result;
} catch (KNXException e) {
logger.debug("Could not {} of {}: {}", task, address, e.getMessage());
+ try {
+ // avoid trashing the log on connection loss
+ Thread.sleep(1000);
+ } catch (InterruptedException ignored) {
+ }
} catch (InterruptedException e) {
logger.trace("Interrupted to {}", task);
return null;
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;
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;
@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;
+ private final ThingUID thingUID;
+
+ @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;
this.port = port;
this.localEndPoint = localEndPoint;
this.useNAT = useNAT;
+ this.secureRoutingBackboneGroupKey = secureRoutingBackboneGroupKey;
+ this.secureRoutingLatencyToleranceMs = secureRoutingLatencyToleranceMs;
+ this.secureTunnelDevKey = secureTunnelDevKey;
+ this.secureTunnelUser = secureTunnelUser;
+ this.secureTunnelUserKey = secureTunnelUserKey;
+ this.thingUID = thingUID;
+ tcpSession = null;
}
@Override
}
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 {
throw new KNXException("no local host available");
}
}
- conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT);
+ if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
+ logger.trace("creating new TCP connection");
+ if (tcpSession != null) {
+ logger.debug("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());
+ 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 {
}
}
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("Bridge {} closing TCP connection", thingUID);
+ try {
+ toBeClosed.close();
+ try {
+ Thread.sleep(PAUSE_ON_TCP_SESSION_CLOSE_MS);
+ } catch (InterruptedException e) {
+ }
+ } catch (Exception e) {
+ logger.debug("closing TCP connection failed: {}", e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ protected void releaseConnection() {
+ closeTcpConnection();
+ super.releaseConnection();
+ }
}
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;
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;
+ }
}
}
}
} 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());
}
}
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);
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import tuwien.auto.calimero.secure.KnxSecureException;
+
/**
* The {@link IPBridgeThingHandler} is responsible for handling commands, which are
* sent to one of the channels. It implements a KNX/IP Gateway, that either acts a a
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);
- private @Nullable IPClient client;
+ private @Nullable IPClient client = null;
private final NetworkAddressService networkAddressService;
public IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressService) {
@Override
public void initialize() {
+ // initialisation would take too long and show a warning during binding startup
+ // KNX secure is adding serious delay
+ updateStatus(ThingStatus.UNKNOWN);
+ initJob = scheduler.submit(() -> {
+ initializeLater();
+ });
+ }
+
+ public void initializeLater() {
IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
+ boolean securityAvailable = false;
+ try {
+ securityAvailable = initializeSecurity(config.getRouterBackboneKey(),
+ config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword());
+ if (securityAvailable) {
+ logger.debug("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.debug("KNX secure: tunnel keys are {} set", (tunnelOk ? "properly" : "not"));
+ } else {
+ logger.debug("KNX security not configured");
+ }
+ } catch (KnxSecureException e) {
+ logger.debug("{}, {}", thing.getUID(), 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(),
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) {
+ logger.warn("Bridge {} missing security configuration for secure tunnel", thing.getUID());
+ 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) {
+ logger.warn("Bridge {} incomplete security configuration for secure tunnel", thing.getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Security configuration for secure tunnel is incomplete");
+ return;
+ }
+
+ logger.debug("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) {
+ logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
+ 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
+ logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "backboneGroupKey required for secure routing; please configure");
+ return;
+ }
+ logger.debug("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));
+ logger.debug("Bridge {} unknown connection type", thing.getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MessageFormat.format(
+ "Unknown IP connection type {0}. Known types are either 'TUNNEL', 'ROUTER', 'SECURETUNNEL', or 'SECUREROUTER'",
+ connectionTypeString));
return;
}
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("Bridge {} completed KNX scheduled initialization", thing.getUID());
}
@Override
public void dispose() {
- super.dispose();
+ final var tmpInitJob = initJob;
+ if (tmpInitJob != null) {
+ while (!tmpInitJob.isDone()) {
+ logger.trace("Bridge {}, shutdown during init, trying to cancel", thing.getUID());
+ tmpInitJob.cancel(true);
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ logger.trace("Bridge {}, cancellation interrupted", thing.getUID());
+ }
+ }
+ initJob = null;
+ }
final var tmpClient = client;
if (tmpClient != null) {
tmpClient.dispose();
client = null;
}
+ super.dispose();
}
@Override
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<IndividualAddress, Destination> 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) throws KnxSecureException {
+ 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 config
+ String key = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", "");
+ if (!key.isEmpty()) {
+ // helper may throw KnxSecureException
+ secureRouting.backboneGroupKey = secHelperParseBackboneKey(key);
+ 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 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", e);
+ }
+ return parsed;
+ }
+
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Nothing to do here
@Override
public void dispose() {
- super.dispose();
client.dispose();
+ super.dispose();
}
@Override
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. Optional. 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
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 = 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 = Tunnel device authentication for secure tunnel mode.
+thing-type.config.knx.ip.tunnelUserId.label = Tunnel user id
+thing-type.config.knx.ip.tunnelUserId.description = Tunnel user id for secure tunnel mode.
+thing-type.config.knx.ip.tunnelUserPassword.label = Tunnel user password
+thing-type.config.knx.ip.tunnelUserPassword.description = 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, use advanced options to configure)
+thing-type.config.knx.ip.type.option.SECUREROUTER = Secure router (experimental, use advanced options to configure)
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
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.
<description>This is a KNX IP interface or router</description>
<config-description>
+ <parameter-group name="knxsecure">
+ <label>KNX secure</label>
+ <description>Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation.</description>
+ </parameter-group>
<parameter name="type" type="text" required="true">
<label>IP Connection Type</label>
- <description>The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER</description>
+ <description>The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or
+ SECUREROUTER</description>
<options>
<option value="TUNNEL">Tunnel</option>
<option value="ROUTER">Router</option>
+ <option value="SECURETUNNEL">Secure tunnel (experimental, use advanced options to configure)</option>
+ <option value="SECUREROUTER">Secure router (experimental, use advanced options to configure)</option>
</options>
</parameter>
<parameter name="ipAddress" type="text">
<description>Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s</description>
<default>60</default>
</parameter>
+ <parameter name="routerBackboneKey" type="text" groupName="knxsecure">
+ <context>password</context>
+ <label>Router backbone key</label>
+ <description>Backbone key for secure router mode. 16 bytes
+ in hex notation. Can also be found
+ in ETS security report.</description>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="tunnelUserId" type="text" groupName="knxsecure">
+ <label>Tunnel user id</label>
+ <description>Tunnel user id for secure tunnel mode.</description>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="tunnelUserPassword" type="text" groupName="knxsecure">
+ <context>password</context>
+ <label>Tunnel user password</label>
+ <description>Tunnel user key for secure tunnel mode.</description>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="tunnelDeviceAuthentication" type="text" groupName="knxsecure">
+ <context>password</context>
+ <label>Tunnel device authentication</label>
+ <description>Tunnel device authentication for secure tunnel mode.</description>
+ <advanced>true</advanced>
+ </parameter>
</config-description>
</bridge-type>
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.knx.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Bridge;
+
+import tuwien.auto.calimero.secure.KnxSecureException;
+
+/**
+ *
+ * @author Holger Friedrich - initial contribution
+ *
+ */
+@NonNullByDefault
+public class KNXBridgeBaseThingHandlerTest {
+
+ @Test
+ public void testSecurityHelpers() {
+ // now check router settings:
+ String bbKeyHex = "D947B12DDECAD528B1D5A88FD347F284";
+ byte[] bbKeyParsedLower = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex.toLowerCase());
+ byte[] bbKeyParsedUpper = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex);
+ assertEquals(16, bbKeyParsedUpper.length);
+ assertArrayEquals(bbKeyParsedUpper, bbKeyParsedLower);
+ }
+
+ @Test
+ @SuppressWarnings("null")
+ public void testInitializeSecurity() {
+ Bridge bridge = mock(Bridge.class);
+ NetworkAddressService nas = mock(NetworkAddressService.class);
+ IPBridgeThingHandler handler = new IPBridgeThingHandler(bridge, nas);
+
+ // no config given
+ assertFalse(handler.initializeSecurity("", "", "", ""));
+
+ // router password configured, length must be 16 bytes in hex notation
+ assertTrue(handler.initializeSecurity("D947B12DDECAD528B1D5A88FD347F284", "", "", ""));
+ assertTrue(handler.initializeSecurity("0xD947B12DDECAD528B1D5A88FD347F284", "", "", ""));
+ assertThrows(KnxSecureException.class, () -> {
+ handler.initializeSecurity("wrongLength", "", "", "");
+ });
+
+ // tunnel configuration
+ assertTrue(handler.initializeSecurity("", "da", "1", "pw"));
+ // cTunnelUser is restricted to a number >0
+ assertThrows(KnxSecureException.class, () -> {
+ handler.initializeSecurity("", "da", "0", "pw");
+ });
+ assertThrows(KnxSecureException.class, () -> {
+ handler.initializeSecurity("", "da", "eins", "pw");
+ });
+ // at least one setting for tunnel is given, count as try to configure secure tunnel
+ // plausibility is checked during initialize()
+ assertTrue(handler.initializeSecurity("", "da", "", ""));
+ assertTrue(handler.initializeSecurity("", "", "1", ""));
+ assertTrue(handler.initializeSecurity("", "", "", "pw"));
+ }
+}