Skip to content

Commit

Permalink
[knx] Add support for KNX IP Secure [WIP]
Browse files Browse the repository at this point in the history
* add support for KNX IP Secure, new options SECURETUNNEL and
  SECUREROUTER, refers to openhab#8872
* add config options for credentials for secure connections
* update user documentation

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
  • Loading branch information
holgerfriedrich committed May 9, 2022
1 parent 553fcfa commit 9d8dfe1
Show file tree
Hide file tree
Showing 12 changed files with 485 additions and 42 deletions.
26 changes: 25 additions & 1 deletion bundles/org.openhab.binding.knx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`: \<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 |
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -92,6 +93,9 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
private final Set<GroupAddressListener> groupAddressListeners = new CopyOnWriteArraySet<>();
private final LinkedBlockingQueue<ReadDatapoint> readDatapoints = new LinkedBlockingQueue<>();

private boolean firstConnect;
private long lastDisconnectSysMillis;

@FunctionalInterface
private interface ListenerNotification {
void apply(BusMessageListener listener, IndividualAddress source, GroupAddress destination, byte[] asdu);
Expand All @@ -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");
}

Expand Down Expand Up @@ -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() {
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -243,6 +282,7 @@ private void releaseConnection() {
rc.removeProcessListener(processListener);
rc.detach();
});
link = nullify(link, l -> l.close());
}

private <T> @Nullable T nullify(T target, @Nullable Consumer<T> lastWill) {
Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 9d8dfe1

Please sign in to comment.