Skip to content

Commit 021cd88

Browse files
[knx] add initial support for KNX secure [WIP]
* add support for KNX IP Secure, new options SECURETUNNEL and SECUREROUTER * add config options for keyring file and password, and credentials for secure connections * add passive (listening only) access for KNX data secure frames, #8872 * add tests for security functions * add useCEMI option for newer serial devices like KNX RF sticks, kBerry, etc., inspired by #10407 * update user documentation * handle exceptions during initial read, fixes #7239 * reduce number of warnings Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
1 parent f70290a commit 021cd88

32 files changed

Lines changed: 1230 additions & 102 deletions

bundles/org.openhab.binding.knx/README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b
2929

3030
| Name | Required | Description | Default value |
3131
|---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
32-
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL` or `ROUTER`) | - |
32+
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL`, `ROUTER`, `SECURETUNNEL` or `SECUREROUTER`) | - |
3333
| 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 |
3434
| portNumber | for `TUNNEL` | Port number of the KNX/IP gateway | 3671 |
3535
| 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 |
@@ -52,6 +52,7 @@ The *serial* bridge accepts the following configuration parameters:
5252
| responseTimeout | N | Timeout in seconds to wait for a response from the KNX bus | 10 |
5353
| readRetriesLimit | N | Limits the read retries while initialization from the KNX bus | 3 |
5454
| autoReconnectPeriod | N | Seconds between connect retries when KNX link has been lost, 0 means never retry | 0 |
55+
| useCemi | N | Use newer CEMI message format, could be useful for newer devices like KNX RF sticks, kBerry, etc. | false |
5556

5657
## Things
5758

@@ -199,6 +200,36 @@ Each configuration parameter has a `mainGA` where commands are written to and op
199200
The `dpt` element is optional. If ommitted, the corresponding default value will be used (see the channel descriptions above).
200201

201202

203+
## KNX Secure
204+
205+
> Note: Support for KNX Secure is partly implemented for openHAB and should be considered as experimental.
206+
207+
### KNX IP Secure
208+
209+
KNX IP Secure protects the traffic between openHAB and your KNX installation. It requires either a KNX Secure Router or a Secure IP Interface with security features enabled in ETS tool.
210+
211+
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. There are two possible ways to provide the key to openHAB:
212+
213+
- The backbone key can be extracted Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `backboneKey`.
214+
- The backbone key is included in ETS keyring export (ETS, project settings, export keyring). Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and also requires `keyringPasswort`.
215+
216+
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:
217+
218+
- All information can be looked up in ETS and provided separately: `tunnelDevAuth`, `tunnelPasswort`. `tunnelUser` 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, ...)
219+
- All necessary information is included in ETS keyring export (ETS, project settings, export keyring). Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and `keyringPasswort`. In addition, `tunnelSourceAddr` needs to be set to uniquely identify the tunnel in use.
220+
221+
222+
### KNX Data Secure
223+
224+
Data secure protects the content of messages on the KNX bus. In a KNX installation, both classic and secure group addresses can coexist.
225+
226+
openHAB typically ignores messages with secure group addresses, unless data secure is configured.
227+
228+
> NOTE: openHAB currently does fully support passive (listening) access to secure group addresses. Write access to secured GAs is done via the "GO diagnostics" feature described in KNX AN170 and is currently limited. Expect a timeout if a data value is written too often. Initial/periodic read will fail, avoid automatic read (< in thing definition).
229+
230+
All necessary information to decode secure group addresses is included in ETS keyring export (ETS, project settings, export keyring). Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and also requires `keyringPasswort`.
231+
232+
202233
## Examples
203234

204235
The following two templates are sufficient for almost all purposes.

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Set;
1919
import java.util.stream.Stream;
2020

21+
import org.eclipse.jdt.annotation.NonNullByDefault;
2122
import org.openhab.core.thing.ThingTypeUID;
2223

2324
/**
@@ -26,6 +27,7 @@
2627
*
2728
* @author Karel Goderis - Initial contribution
2829
*/
30+
@NonNullByDefault
2931
public class KNXBindingConstants {
3032

3133
public static final String BINDING_ID = "knx";
@@ -51,6 +53,14 @@ public class KNXBindingConstants {
5153
public static final String LOCAL_SOURCE_ADDRESS = "localSourceAddr";
5254
public static final String PORT_NUMBER = "portNumber";
5355
public static final String SERIAL_PORT = "serialPort";
56+
public static final String USE_CEMI = "useCemi";
57+
public static final String KEYRING_FILE = "keyringFile";
58+
public static final String KEYRING_PASSWORD = "keyringPassword";
59+
public static final String ROUTER_BACKBONE_GROUP_KEY = "routerBackboneGroupKey";
60+
public static final String TUNNEL_USER_ID = "tunnelUserId";
61+
public static final String TUNNEL_USER_PASSWORD = "tunnelUserPassword";
62+
public static final String TUNNEL_DEVICE_AUTHENTICATION = "tunnelDeviceAuthentication";
63+
public static final String TUNNEL_SOURCE_ADDRESS = "tunnelSourceAddress";
5464

5565
// The default multicast ip address (see <a
5666
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXTypeMapper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import org.openhab.core.types.Type;
1818

1919
import tuwien.auto.calimero.datapoint.Datapoint;
20-
import tuwien.auto.calimero.process.ProcessEvent;
2120

2221
/**
2322
* This interface must be implemented by classes that provide a type mapping
@@ -45,7 +44,8 @@ public interface KNXTypeMapper {
4544
* maps a datapoint value to an openHAB command or state
4645
*
4746
* @param datapoint the source datapoint
48-
* @param data the datapoint value as an ASDU byte array (see <code>{@link ProcessEvent}.getASDU()</code>)
47+
* @param data the datapoint value as an ASDU byte array (see
48+
* <code>{@link tuwien.auto.calimero.process.ProcessEvent}.getASDU()</code>)
4949
* @return a command or state of openHAB
5050
*/
5151
@Nullable

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/AbstractSpec.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414

1515
import org.eclipse.jdt.annotation.NonNullByDefault;
1616
import org.eclipse.jdt.annotation.Nullable;
17-
import org.openhab.binding.knx.internal.client.InboundSpec;
18-
import org.openhab.binding.knx.internal.client.OutboundSpec;
1917

2018
import tuwien.auto.calimero.GroupAddress;
2119
import tuwien.auto.calimero.KNXFormatException;
@@ -57,7 +55,8 @@ protected final GroupAddress toGroupAddress(GroupAddressConfiguration ga) {
5755
/**
5856
* Return the data point type.
5957
* <p>
60-
* See {@link InboundSpec#getDPT()} and {@link OutboundSpec#getDPT()}.
58+
* See {@link org.openhab.binding.knx.internal.client.InboundSpec#getDPT()} and
59+
* {@link org.openhab.binding.knx.internal.client.OutboundSpec#getDPT()}.
6160
*
6261
* @return the data point type.
6362
*/

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/ListenSpecImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.Collections;
1818
import java.util.List;
1919

20+
import org.eclipse.jdt.annotation.NonNullByDefault;
2021
import org.eclipse.jdt.annotation.Nullable;
2122
import org.openhab.binding.knx.internal.client.InboundSpec;
2223

@@ -28,6 +29,7 @@
2829
* @author Simon Kaufmann - initial contribution and API.
2930
*
3031
*/
32+
@NonNullByDefault
3133
public class ListenSpecImpl extends AbstractSpec implements InboundSpec {
3234

3335
private final List<GroupAddress> listenAddresses;

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class TypeColor extends KNXChannelType {
4040

4141
@Override
4242
protected Set<String> getAllGAKeys() {
43-
return Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA, HSB_GA).collect(toSet());
43+
final var tmp = Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA, HSB_GA).collect(toSet());
44+
return tmp;
4445
}
4546

4647
@Override

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class TypeDimmer extends KNXChannelType {
4040

4141
@Override
4242
protected Set<String> getAllGAKeys() {
43-
return Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA).collect(toSet());
43+
final var tmp = Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA).collect(toSet());
44+
return tmp;
4445
}
4546

4647
@Override

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ protected String getDefaultDPT(String gaConfigKey) {
5353

5454
@Override
5555
protected Set<String> getAllGAKeys() {
56-
return Stream.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA).collect(toSet());
56+
final var tmp = Stream.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA).collect(toSet());
57+
return tmp;
5758
}
5859
}

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import tuwien.auto.calimero.GroupAddress;
4040
import tuwien.auto.calimero.IndividualAddress;
4141
import tuwien.auto.calimero.KNXException;
42+
import tuwien.auto.calimero.KnxRuntimeException;
4243
import tuwien.auto.calimero.datapoint.CommandDP;
4344
import tuwien.auto.calimero.datapoint.Datapoint;
4445
import tuwien.auto.calimero.device.ProcessCommunicationResponder;
@@ -54,6 +55,7 @@
5455
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
5556
import tuwien.auto.calimero.process.ProcessEvent;
5657
import tuwien.auto.calimero.process.ProcessListener;
58+
import tuwien.auto.calimero.secure.KnxSecureException;
5759
import tuwien.auto.calimero.secure.SecureApplicationLayer;
5860
import tuwien.auto.calimero.secure.Security;
5961

@@ -78,6 +80,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
7880
private final int readRetriesLimit;
7981
private final StatusUpdateCallback statusUpdateCallback;
8082
private final ScheduledExecutorService knxScheduler;
83+
protected final Security openhabSecurity;
8184

8285
private @Nullable ProcessCommunicator processCommunicator;
8386
private @Nullable ProcessCommunicationResponder responseCommunicator;
@@ -91,6 +94,9 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
9194
private final Set<GroupAddressListener> groupAddressListeners = new CopyOnWriteArraySet<>();
9295
private final LinkedBlockingQueue<ReadDatapoint> readDatapoints = new LinkedBlockingQueue<>();
9396

97+
private boolean firstConnect;
98+
private long lastDisconnectSysMillis;
99+
94100
@FunctionalInterface
95101
private interface ListenerNotification {
96102
void apply(BusMessageListener listener, IndividualAddress source, GroupAddress destination, byte[] asdu);
@@ -101,6 +107,7 @@ private interface ListenerNotification {
101107

102108
@Override
103109
public void detached(DetachEvent e) {
110+
lastDisconnectSysMillis = System.currentTimeMillis();
104111
logger.debug("The KNX network link was detached from the process communicator");
105112
}
106113

@@ -127,14 +134,18 @@ public void groupReadResponse(ProcessEvent e) {
127134
};
128135

129136
public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause,
130-
int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) {
137+
int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback,
138+
Security openhabSecurity) {
131139
this.autoReconnectPeriod = autoReconnectPeriod;
132140
this.thingUID = thingUID;
133141
this.responseTimeout = responseTimeout;
134142
this.readingPause = readingPause;
135143
this.readRetriesLimit = readRetriesLimit;
136144
this.knxScheduler = knxScheduler;
137145
this.statusUpdateCallback = statusUpdateCallback;
146+
this.openhabSecurity = openhabSecurity;
147+
firstConnect = true;
148+
lastDisconnectSysMillis = System.currentTimeMillis();
138149
}
139150

140151
public void initialize() {
@@ -145,7 +156,9 @@ public void initialize() {
145156

146157
private boolean scheduleReconnectJob() {
147158
if (autoReconnectPeriod > 0) {
148-
connectJob = knxScheduler.schedule(this::connect, autoReconnectPeriod, TimeUnit.SECONDS);
159+
// schedule connect job, for the first connection ignore autoReconnectPeriod and use 1 sec
160+
connectJob = knxScheduler.schedule(this::connect, firstConnect ? 1 : autoReconnectPeriod, TimeUnit.SECONDS);
161+
firstConnect = false;
149162
return true;
150163
} else {
151164
return false;
@@ -173,6 +186,12 @@ private synchronized boolean connect() {
173186
if (isConnected()) {
174187
return true;
175188
}
189+
long now = System.currentTimeMillis();
190+
if ((now - lastDisconnectSysMillis) < 1000) {
191+
logger.debug("fast reconnect");
192+
// SECURETUNNEL: TCP Connections need additional efforts to avoid grabbing all tunnel connections
193+
// the interface, this is implemented in IPClient
194+
}
176195
try {
177196
releaseConnection();
178197

@@ -195,7 +214,7 @@ private synchronized boolean connect() {
195214
this.processCommunicator = processCommunicator;
196215

197216
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
198-
new SecureApplicationLayer(link, Security.defaultInstallation()));
217+
new SecureApplicationLayer(link, openhabSecurity));
199218
this.responseCommunicator = responseCommunicator;
200219

201220
link.addLinkListener(this);
@@ -206,7 +225,7 @@ private synchronized boolean connect() {
206225
statusUpdateCallback.updateStatus(ThingStatus.ONLINE);
207226
connectJob = null;
208227
return true;
209-
} catch (KNXException | InterruptedException e) {
228+
} catch (KNXException | InterruptedException | KnxSecureException e) {
210229
logger.debug("Error connecting to the bus: {}", e.getMessage(), e);
211230
disconnect(e);
212231
scheduleReconnectJob();
@@ -215,18 +234,19 @@ private synchronized boolean connect() {
215234
}
216235

217236
private void disconnect(@Nullable Exception e) {
237+
lastDisconnectSysMillis = System.currentTimeMillis();
238+
218239
releaseConnection();
219240
if (e != null) {
220-
String message = e.getLocalizedMessage();
221241
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
222-
message != null ? message : "");
242+
"" + e.getLocalizedMessage());
223243
} else {
224244
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
225245
}
226246
}
227247

228248
@SuppressWarnings("null")
229-
private void releaseConnection() {
249+
protected void releaseConnection() {
230250
logger.debug("Bridge {} is disconnecting from the KNX bus", thingUID);
231251
readDatapoints.clear();
232252
busJob = nullify(busJob, j -> j.cancel(true));
@@ -244,7 +264,7 @@ private void releaseConnection() {
244264
});
245265
}
246266

247-
private <T> T nullify(T target, @Nullable Consumer<T> lastWill) {
267+
private <T> @Nullable T nullify(T target, @Nullable Consumer<T> lastWill) {
248268
if (target != null && lastWill != null) {
249269
lastWill.accept(target);
250270
}
@@ -275,6 +295,7 @@ private String toDPTValue(Type type, String dpt) {
275295
return typeHelper.toDPTValue(type, dpt);
276296
}
277297

298+
// datapoint is null at end of the list, warning is misleading
278299
@SuppressWarnings("null")
279300
private void readNextQueuedDatapoint() {
280301
if (!connectIfNotAutomatic()) {
@@ -285,7 +306,15 @@ private void readNextQueuedDatapoint() {
285306
return;
286307
}
287308
ReadDatapoint datapoint = readDatapoints.poll();
309+
288310
if (datapoint != null) {
311+
// TODO #8872: allow write access, currently only listening mode
312+
if (openhabSecurity.groupKeys().containsKey(datapoint.getDatapoint().getMainAddress())) {
313+
logger.debug("outgoing secure communication not implemented, explicit read from GA '{}' skipped",
314+
datapoint.getDatapoint().getMainAddress());
315+
return;
316+
}
317+
289318
datapoint.incrementRetries();
290319
try {
291320
logger.trace("Sending a Group Read Request telegram for {}", datapoint.getDatapoint().getMainAddress());
@@ -299,6 +328,11 @@ private void readNextQueuedDatapoint() {
299328
logger.warn("Giving up reading datapoint {}, the number of maximum retries ({}) is reached.",
300329
datapoint.getDatapoint().getMainAddress(), datapoint.getLimit());
301330
}
331+
} catch (KnxRuntimeException e) {
332+
// KnxRuntimeException is _not_ a subclass of KnxException. Fail gracefully for this case as well,
333+
// fixes #7239
334+
logger.warn("Error reading datapoint {}: {}", datapoint.getDatapoint().getMainAddress(),
335+
e.getMessage());
302336
} catch (InterruptedException e) {
303337
logger.debug("Interrupted sending KNX read request");
304338
return;
@@ -392,7 +426,8 @@ public final boolean unregisterGroupAddressListener(GroupAddressListener listene
392426

393427
@Override
394428
public boolean isConnected() {
395-
return link != null && link.isOpen();
429+
final var tmpLink = link;
430+
return tmpLink != null && tmpLink.isOpen();
396431
}
397432

398433
@Override
@@ -449,6 +484,12 @@ private void sendToKNX(ProcessCommunication communicator, KNXNetworkLink link, G
449484
return;
450485
}
451486

487+
// TODO #8872: allow write access, currently only listening mode
488+
if (openhabSecurity.groupKeys().containsKey(groupAddress)) {
489+
logger.debug("outgoing secure communication not implemented, write to GA '{}' skipped", groupAddress);
490+
return;
491+
}
492+
452493
Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0, dpt);
453494
String mappedValue = toDPTValue(type, dpt);
454495

bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
package org.openhab.binding.knx.internal.client;
1414

15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
1517
import tuwien.auto.calimero.KNXException;
1618
import tuwien.auto.calimero.knxnetip.KNXnetIPConnection;
1719
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
@@ -24,9 +26,11 @@
2426
* @author Simon Kaufmann - initial contribution and API
2527
*
2628
*/
29+
@NonNullByDefault
2730
public class CustomKNXNetworkLinkIP extends KNXNetworkLinkIP {
2831

2932
public static final int TUNNELING = KNXNetworkLinkIP.TUNNELING;
33+
public static final int TUNNELINGV2 = KNXNetworkLinkIP.TunnelingV2;
3034
public static final int ROUTING = KNXNetworkLinkIP.ROUTING;
3135

3236
CustomKNXNetworkLinkIP(final int serviceMode, KNXnetIPConnection conn, KNXMediumSettings settings)

0 commit comments

Comments
 (0)