Skip to content

Commit

Permalink
[snmp] Upgrades and enhancements
Browse files Browse the repository at this point in the history
- 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 committed Feb 3, 2023
1 parent 4d9bf1c commit f67bfbe
Show file tree
Hide file tree
Showing 22 changed files with 777 additions and 177 deletions.
84 changes: 61 additions & 23 deletions bundles/org.openhab.binding.snmp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ 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 @@ -25,8 +29,8 @@ This can be done either by software like _snmptrapd_ or by adding a firewall rul
iptables -t nat -I PREROUTING --src 0/0 --dst 192.168.0.10 -p udp --dport 162 -j REDIRECT --to-ports 8162
```

would forward all TCP packets addressed to 192.168.0.10 from port 162 to 8162.
Check with your operating system manual how to make that change permanent.
would forward all TCP packets addressed to 192.168.0.10 from port 162 to 8162.
Check with your operating system manual how to make that change permanent.

Example configuration for using port 8162:

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.

Optional configuration parameters 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 rquired, 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 Down Expand Up @@ -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 f67bfbe

Please sign in to comment.