Skip to content

Commit

Permalink
[snmp] Upgrades and enhancements (openhab#14330)
Browse files Browse the repository at this point in the history
* [snmp] Upgrades and enhancements

- bug: improve test stability
- enhancement: add support for UoM
- bug: fix misleading error message
- bug: fix initialization exceptions
- enhancement: Add support for SNMPv3
- enhancement: add opaque value handling

Signed-off-by: Jan N. Klug <github@klug.nrw>
  • Loading branch information
J-N-K authored and nemerdaud committed Feb 28, 2023
1 parent d10fe1c commit 52f7f4e
Show file tree
Hide file tree
Showing 23 changed files with 809 additions and 178 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@
/bundles/org.openhab.binding.smhi/ @pacive
/bundles/org.openhab.binding.smsmodem/ @dalgwen
/bundles/org.openhab.binding.sncf/ @clinique
/bundles/org.openhab.binding.snmp/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.snmp/ @J-N-K
/bundles/org.openhab.binding.solaredge/ @alexf2015
/bundles/org.openhab.binding.solarlog/ @johannrichard
/bundles/org.openhab.binding.solarmax/ @jamietownsend
Expand Down
86 changes: 62 additions & 24 deletions bundles/org.openhab.binding.snmp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

This binding integrates the Simple Network Management Protocol (SNMP).
SNMP can be used to monitor or control a large variety of network equipment, e.g. routers, switches, NAS-systems.
Currently protocol version 1 and 2c are supported.
Currently, protocol version 1 and 2c are supported.

## Supported Things

Only one thing is supported: `target`.
It represents a single network device.
There are two supported things:

- `target` for SNMP v1/v2c agents
- `target3` for SNMP v3 agents

Both represent a single network device.
Things can be extended with `number`, `string` and `switch` channels.

## Binding Configuration
Expand All @@ -17,7 +21,7 @@ In this case the `port` parameter defaults to `0`.

For receiving traps a port for receiving traps needs to be configured.
The standard port for receiving traps is 162, however binding to ports lower than 1024 is only allowed with privileged right on most *nix systems.
Therefore it is recommended to bind to a port higher than 1024 (e.g. 8162).
Therefore, it is recommended to bind to a port higher than 1024 (e.g. 8162).
In case the trap sending equipment does not allow to change the destination port (e.g. Mikrotik routers), it is necessary to forward the received packets to the new port.
This can be done either by software like _snmptrapd_ or by adding a firewall rule to your system, e.g. by executing

Expand All @@ -40,19 +44,11 @@ port=8162

## Thing Configuration

The `target` thing has one mandatory parameter: `hostname`.
It can be set as FQDN or IP address.

Optional configuration parameters are `community`, `version` and `refresh`.

The SNMP community can be set with the `community` parameter.
It defaults to `public`.
### Common parameters for all thing-types

Currently two protocol versions are supported.
The protocol version can be set with the `protocol` parameter.
The allowed values are `v1` or `V1`for v1 and `v2c` or `V2C` for v2c.
The default is `v1`.
The `hostname` is mandatory and can be set as FQDN or IP address.

An optional configuration parameter is `refresh`.
By using the `refresh` parameter the time between two subsequent GET requests to the target can be set.
The default is `60` for 60s.

Expand All @@ -67,6 +63,44 @@ A single request times out after `timeout` ms.
After `retries` timeouts the refresh operation is considered to be fails and the status of the thing set accordingly.
The default values are `timeout=1500` and `retries=2`.

### `target`

The `target` thing has two optional configuration parameters: `community` and `version`.

The SNMP community for SNMP version 2c can be set with the `community` parameter.
It defaults to `public`.

Currently two protocol versions are supported.
The protocol version can be set with the `protocol` parameter.
The allowed values are `v1` or `V1` for v1 and `v2c` or `V2C` for v2c.
The default is `v1`.

### `target3`

The `target3` thing has additional mandatory parameters: `engineId` and `user`.

The `engineId` must be given in hexadecimal notation (case-insensitive) without separators (e.g. `80000009035c710dbcd9e6`).
The allowed length is 11 to 32 bytes (22 to 64 hex characters).
If you encounter problems, please check if your agent prefixes the set engine id (e.g. Mikrotik uses `80003a8c04` and appends the set value to that).

The `user` parameter is named "securityName" or "userName" in most agents.

Optional configuration parameters are: `securityModel`, `authProtocol`, `authPassphrase`, `privProtocol` and `privPassphrase`.

The `securityModel` can be set to

- `NO_AUTH_NO_PRIV` (default) - no encryption on authentication data, no encryption on transmitted data
- `AUTH_NO_PRIV` - encryption on authentication data, no encryption on transmitted data
- `AUTH_PRIV` - encryption on authentication data, encryption on transmitted data

Depending on the `securityModel` some of the other parameters are also mandatory.

If authentication encryption is required, at least `authPassphrase` needs to be set, while `authProtocol` has a default of `MD5`.
Other possible values for `authProtocol` are `SHA`, `HMAC128SHA224`, `HMAC192SHA256`, `HMAC256SHA384` and `HMAC384SHA512`.

If encryption of transmitted data (privacy encryption) is required, at least `privPassphrase` needs to be set, while `privProtocol` defaults to `DES`.
Other possible values for `privProtocol` are `AES128`, `AES192` and `AES256`.

## Channels

The `target` thing has no fixed channels.
Expand All @@ -87,7 +121,7 @@ Using`TRAP` channels requires configuring the receiving port (see "Binding confi
The `datatype` parameter is needed in some special cases where data is written to the target.
The default `datatype` for `number` channels is `UINT32`, representing an unsigned integer with 32 bit length.
Alternatively `INT32` (signed integer with 32 bit length), `COUNTER64` (unsigned integer with 64 bit length) or `FLOAT` (floating point number) can be set.
Floating point numbers have to be supplied (and will be send) as strings.
Floating point numbers have to be supplied (and will be sent) as strings.
For `string` channels the default `datatype` is `STRING` (i.e. the item's will be sent as a string).
If it is set to `IPADDRESS`, an SNMP IP address object is constructed from the item's value.
The `HEXSTRING` datatype converts a hexadecimal string (e.g. `aa bb 11`) to the respective octet string before sending data to the target (and vice versa for receiving data).
Expand All @@ -99,11 +133,16 @@ In `READ`, `READ_WRITE` or `TRAP` mode they change to either `ON` or `OFF` on th
The parameters used for defining the values are `onvalue` and `offvalue`.
The `datatype` parameter is used to convert the configuration strings to the needed values.

| type | item | description |
| ------ | ------ | ------------------------------ |
| number | Number | a channel with a numeric value |
| string | String | a channel with a string value |
| switch | Switch | a channel that has two states |
`number`-type channels have a `unit` parameter.
The unit is added to the received value before it is passed to the channel.
For commands (i.e. sending), the value is first converted to the configured unit.

| type | item | description |
|----------|--------|---------------------------------|
| number | Number | a channel with a numeric value |
| string | String | a channel with a string value |
| switch | Switch | a channel that has two states |


### SNMP Exception (Error) Handling

Expand All @@ -121,10 +160,10 @@ Valid values are all valid values for that channel (i.e. `ON`/`OFF` for a switch

demo.things:

```java
```
Thing snmp:target:router [ hostname="192.168.0.1", protocol="v2c" ] {
Channels:
Type number : inBytes [ oid=".1.3.6.1.2.1.31.1.1.1.6.2", mode="READ", unit="B" ]
Type number : inBytes [ oid=".1.3.6.1.2.1.31.1.1.1.6.2", mode="READ" ]
Type number : outBytes [ oid=".1.3.6.1.2.1.31.1.1.1.10.2", mode="READ" ]
Type number : if4Status [ oid="1.3.6.1.2.1.2.2.1.7.4", mode="TRAP" ]
Type switch : if4Command [ oid="1.3.6.1.2.1.2.2.1.7.4", mode="READ_WRITE", datatype="UINT32", onvalue="2", offvalue="0" ]
Expand All @@ -137,7 +176,6 @@ demo.items:

```java
Number inBytes "Router bytes in [%d]" { channel="snmp:target:router:inBytes" }
Number inGigaBytes "Router gigabytes in [%d GB]" { channel="snmp:target:router:inBytes" }
Number outBytes "Router bytes out [%d]" { channel="snmp:target:router:outBytes" }
Number if4Status "Router interface 4 status [%d]" { channel="snmp:target:router:if4Status" }
Switch if4Command "Router interface 4 switch [%s]" { channel="snmp:target:router:if4Command" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class SnmpBindingConstants {

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_TARGET = new ThingTypeUID(BINDING_ID, "target");
public static final ThingTypeUID THING_TYPE_TARGET3 = new ThingTypeUID(BINDING_ID, "target3");

public static final ChannelTypeUID CHANNEL_TYPE_UID_NUMBER = new ChannelTypeUID(BINDING_ID, "number");
public static final ChannelTypeUID CHANNEL_TYPE_UID_STRING = new ChannelTypeUID(BINDING_ID, "string");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
package org.openhab.binding.snmp.internal;

import static org.openhab.binding.snmp.internal.SnmpBindingConstants.THING_TYPE_TARGET;
import static org.openhab.binding.snmp.internal.SnmpBindingConstants.THING_TYPE_TARGET3;

import java.util.Collections;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand All @@ -37,7 +37,7 @@
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.snmp")
public class SnmpHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_TARGET);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_TARGET, THING_TYPE_TARGET3);

private final SnmpService snmpService;

Expand All @@ -54,7 +54,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_TARGET.equals(thingTypeUID)) {
if (THING_TYPE_TARGET.equals(thingTypeUID) || THING_TYPE_TARGET3.equals(thingTypeUID)) {
return new SnmpTargetHandler(thing, snmpService);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.types.SnmpAuthProtocol;
import org.openhab.binding.snmp.internal.types.SnmpPrivProtocol;
import org.snmp4j.CommandResponder;
import org.snmp4j.PDU;
import org.snmp4j.Target;
Expand All @@ -31,9 +33,12 @@
@NonNullByDefault
public interface SnmpService {

public void addCommandResponder(CommandResponder listener);
void addCommandResponder(CommandResponder listener);

public void removeCommandResponder(CommandResponder listener);
void removeCommandResponder(CommandResponder listener);

public void send(PDU pdu, Target target, @Nullable Object userHandle, ResponseListener listener) throws IOException;
void send(PDU pdu, Target target, @Nullable Object userHandle, ResponseListener listener) throws IOException;

void addUser(String userName, SnmpAuthProtocol snmpAuthProtocol, @Nullable String authPassphrase,
SnmpPrivProtocol snmpPrivProtocol, @Nullable String privPassphrase, byte[] engineId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.config.SnmpServiceConfiguration;
import org.openhab.binding.snmp.internal.types.SnmpAuthProtocol;
import org.openhab.binding.snmp.internal.types.SnmpPrivProtocol;
import org.openhab.core.config.core.Configuration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
Expand All @@ -32,8 +36,13 @@
import org.snmp4j.Snmp;
import org.snmp4j.Target;
import org.snmp4j.event.ResponseListener;
import org.snmp4j.mp.MPv3;
import org.snmp4j.security.Priv3DES;
import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.security.UsmUser;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.DefaultUdpTransportMapping;

Expand All @@ -53,10 +62,18 @@ public class SnmpServiceImpl implements SnmpService {
private @Nullable Snmp snmp;
private @Nullable DefaultUdpTransportMapping transport;

private List<CommandResponder> listeners = new ArrayList<>();
private final List<CommandResponder> listeners = new ArrayList<>();
private final Set<UserEntry> userEntries = new HashSet<>();

@Activate
public SnmpServiceImpl(Map<String, Object> config) {
SecurityProtocols.getInstance().addDefaultProtocols();
SecurityProtocols.getInstance().addPrivacyProtocol(new Priv3DES());

OctetString localEngineId = new OctetString(MPv3.createLocalEngineID());
USM usm = new USM(SecurityProtocols.getInstance(), localEngineId, 0);
SecurityModels.getInstance().addSecurityModel(usm);

modified(config);
}

Expand All @@ -78,9 +95,12 @@ protected void modified(Map<String, Object> config) {
SecurityProtocols.getInstance().addPrivacyProtocol(new Priv3DES());

final Snmp snmp = new Snmp(transport);
listeners.forEach(listener -> snmp.addCommandResponder(listener));
listeners.forEach(snmp::addCommandResponder);
snmp.listen();

// re-add user entries
userEntries.forEach(u -> addUser(snmp, u));

this.snmp = snmp;
this.transport = transport;

Expand All @@ -90,6 +110,7 @@ protected void modified(Map<String, Object> config) {
}
}

@SuppressWarnings("unused")
@Deactivate
public void deactivate() {
try {
Expand Down Expand Up @@ -141,4 +162,37 @@ public void send(PDU pdu, Target target, @Nullable Object userHandle, ResponseLi
logger.warn("SNMP service not initialized, can't send {} to {}", pdu, target);
}
}

@Override
public void addUser(String userName, SnmpAuthProtocol snmpAuthProtocol, @Nullable String authPassphrase,
SnmpPrivProtocol snmpPrivProtocol, @Nullable String privPassphrase, byte[] engineId) {
UsmUser usmUser = new UsmUser(new OctetString(userName), snmpAuthProtocol.getOid(),
authPassphrase != null ? new OctetString(authPassphrase) : null, snmpPrivProtocol.getOid(),
privPassphrase != null ? new OctetString(privPassphrase) : null);
OctetString securityNameOctets = new OctetString(userName);

UserEntry userEntry = new UserEntry(securityNameOctets, new OctetString(engineId), usmUser);
userEntries.add(userEntry);

Snmp snmp = this.snmp;
if (snmp != null) {
addUser(snmp, userEntry);
}
}

private static void addUser(Snmp snmp, UserEntry userEntry) {
snmp.getUSM().addUser(userEntry.securityName, userEntry.engineId, userEntry.user);
}

private static class UserEntry {
public OctetString securityName;
public OctetString engineId;
public UsmUser user;

public UserEntry(OctetString securityName, OctetString engineId, UsmUser user) {
this.securityName = securityName;
this.engineId = engineId;
this.user = user;
}
}
}
Loading

0 comments on commit 52f7f4e

Please sign in to comment.