Here is a README for notification-originator that hopefully helps to solve the issue:
AGENT++ NotificationOriginator: SNMPv3 (USM) TRAPs and INFORMs
==============================================================
This document explains how to configure the NotificationOriginator of an
AGENT++ agent so that it sends SNMPv3 notifications secured by the
User-based Security Model (USM, RFC 3414), both as
- SNMPv2-Trap PDUs ("v3 notifications" / "v3 traps"; unconfirmed), and
- InformRequest PDUs ("informs"; confirmed by the receiver).
It also explains why the two need different USM key localization: a v3
TRAP is authenticated/encrypted with keys localized to the agent's own
(local) snmpEngineID, whereas an INFORM uses keys localized to the
receiver's (remote) snmpEngineID.
The sample code uses this target:
Receiver (notification receiver / trap sink): 192.0.2.10, UDP port 162
USM user: notifyUser
Authentication: HMAC-192-SHA-256,
password "notifyAuthPassword"
Privacy: AES-128,
password "notifyPrivPassword"
Security level: authPriv
====================================================================
1. HOW THE NOTIFICATIONORIGINATOR SELECTS TARGETS
-------------------------------------------------
NotificationOriginator (include/agent_pp/notification_originator.h,
src/notification_originator.cpp) does not keep its own list of receivers.
It is configured completely through standard MIB tables, exactly as
described in RFC 3413:
snmpNotifyTable (SNMP-NOTIFICATION-MIB)
name -> tag + type, where type is trap(1) or inform(2)
snmpTargetAddrTable (SNMP-TARGET-MIB)
name -> transport domain + address, timeout, retry count,
tag list, params name
snmpTargetParamsTable (SNMP-TARGET-MIB)
params name -> mpModel (0=v1, 1=v2c, 3=v3), securityModel
(3=USM), securityName, securityLevel
vacmAccessTable & co. (SNMP-VIEW-BASED-ACM-MIB)
the notify view of the securityName's group must include the
notification OID and every variable binding OID
usmUserTable (SNMP-USER-BASED-SM-MIB)
the (localized) keys of the securityName
When NotificationOriginator::generate() or notify() is called, it walks
all snmpTargetAddrTable rows. For every snmpNotifyTable row whose tag is
contained in the target's tag list it
1. checks the notification against the snmpNotifyFilterTable,
2. reads mpModel, securityModel, securityName and securityLevel from the
snmpTargetParamsTable row referenced by the target,
3. checks VACM "notify" access for that securityName, security level
and the context name passed to generate()/notify() for the
notification OID and all variable bindings,
4. creates a UTarget (without engine ID) for the target address and
sends a TRAP or an INFORM - depending on the snmpNotifyType - through
SnmpRequestV3::send(). For informs, snmpTargetAddrTimeout (1/100 s)
and snmpTargetAddrRetryCount are applied to the UTarget.
The actual USM processing (choosing the authoritative engine ID, engine
ID discovery, key lookup, authentication and encryption) is done by the
v3MP/USM of SNMP++ that is registered with the agent's RequestList.
====================================================================
2. KEY LOCALIZATION: LOCAL ENGINE ID (TRAP) vs. REMOTE ENGINE ID (INFORM)
-------------------------------------------------------------------------
2.1 Background
USM never uses a password directly. Per RFC 3414 (A.2) a password is
first converted into a user key Ku (by hashing 1 MB of the repeated
password), which is then "localized" for one specific SNMP engine:
Kul = H( Ku || snmpEngineID || Ku )
The same user and password therefore results in a different auth key and
a different priv key for every snmpEngineID. A message can only be
verified and decrypted by an entity that holds the keys localized for
the msgAuthoritativeEngineID carried in that message.
Which engine is authoritative depends on the PDU type (RFC 3412, 6.4,
and RFC 3414, 1.5.1):
- If the PDU expects no response (TRAP, Response, Report), the SENDER
is authoritative.
- If the PDU expects a response (Get, GetNext, GetBulk, Set, INFORM),
the RECEIVER is authoritative.
2.2 v3 TRAP (SNMPv2-Trap PDU) - localized with the LOCAL engine ID
- msgAuthoritativeEngineID = the agent's own snmpEngineID, i.e. the
engine ID the v3MP was created with
(v3MP::get_local_engine_id(); the AGENT++ examples create it with
SnmpEngineID::create_engine_id(port)).
- The agent uses the usmUserTable row indexed by
(localEngineID, "notifyUser"). This is the same row that the agent
uses to authenticate incoming requests from that user, so a user that
can query the agent can usually also receive its traps.
- snmpEngineBoots/snmpEngineTime in the message are the agent's own
values. No engine ID discovery and no time synchronization takes
place; the trap is sent immediately.
- The RECEIVER must be configured with the user's keys localized for
the AGENT's engine ID. Because a trap receiver usually talks to many
agents, it needs one user entry per agent engine ID (or the
passwords, so it can localize on demand). With Net-SNMP's snmptrapd
this looks like:
createUser -e 0x<agent engine ID in hex> notifyUser SHA-256 \
notifyAuthPassword AES notifyPrivPassword
- The agent never learns whether the trap was accepted. A wrong engine
ID, wrong password or wrong protocol at the receiver is a silent
failure on the agent side (look at the receiver's usmStats counters).
- Keep the agent's engine ID stable (also across restarts and changes
of the listen port), otherwise all receivers have to be reconfigured.
Also increment and persist snmpEngineBoots on every start (see
Mib::get_boot_counter()/set_boot_counter() in the examples), because
receivers may reject traps from an engine whose boots/time went
backwards.
2.3 INFORM (InformRequest PDU) - localized with the REMOTE engine ID
- msgAuthoritativeEngineID = the RECEIVER's snmpEngineID.
- The agent must therefore know the receiver's engine ID, boots and
time. Since NotificationOriginator creates the UTarget without engine
ID, SNMP++ performs the RFC 3414 (4.) discovery on the first inform
to that address:
1. a noAuthNoPriv probe with an empty engine ID is sent and the
receiver answers with a usmStatsUnknownEngineIDs Report
carrying its snmpEngineID,
2. for authenticated users, the receiver's snmpEngineBoots/Time are
learned from a usmStatsNotInTimeWindows Report,
3. the actual inform is sent and the Response is awaited.
The discovered engine ID is cached in the v3MP engine ID table
(per IP address and port), the time values in the USM time table.
- The agent then needs keys for (remoteEngineID, "notifyUser"). SNMP++
looks for such a localized entry in its usmUserTable. If there is
none, it looks up the user in its usmUserNameTable, which holds the
passwords; if found, it localizes the keys for the remote engine ID
on the fly and adds the new localized user (AGENT++ also adds it as a
volatile row to its UsmUserTable). If neither exists, the inform
cannot be sent.
- Consequently the agent needs EITHER
(a) the user's PASSWORDS registered with the USM
(UsmUserTable::addNewRow(..., addPasswordsToUSM = true)), so
keys can be localized for any receiver discovered later, OR
(b) a usmUserTable row whose keys are explicitly localized for the
known engine ID of the receiver.
A row that is only localized for the local engine ID (enough for v3
traps) is NOT enough for informs.
- The RECEIVER uses keys localized to its OWN engine ID, i.e. the same
user configuration it would use for incoming GET/SET requests. With
snmptrapd this is a createUser line without "-e":
createUser notifyUser SHA-256 notifyAuthPassword \
AES notifyPrivPassword
- Informs are confirmed. Errors at the receiver (unknown user, wrong
digest, decryption error) are reported back and the inform is
retransmitted according to snmpTargetAddrRetryCount; the status is
returned by generate()/notify().
2.4 Summary
v3 TRAP INFORM
----------------------- ------------------------ ------------------------
authoritative engine agent (sender) receiver
keys localized with local engine ID remote engine ID
agent needs usmUserTable row for passwords in USM, or row
(localEngineID, user) for (remoteEngineID, user)
receiver needs keys for the AGENT's keys for its OWN
engine ID ("-e" agentID) engine ID
discovery / time sync no yes (first inform, or
skip with engine ID table)
delivery confirmed no yes (Response, retries)
generate() blocks no yes, until Response or
timeout * (retries + 1)
====================================================================
3. SAMPLE CODE
--------------
The following code follows the structure of the agents in examples/
(e.g. examples/static_table/src/agent.cpp): it assumes that the Mib, the
RequestList, the v3MP and the Vacm have been created and registered as
shown there, i.e.
v3MP *v3mp = new v3MP(engineId, snmpEngineBoots, stat);
reqList = new RequestList(mib);
reqList->set_v3mp(v3mp);
snmp.set_mpv3(v3mp);
mib->set_request_list(reqList);
...
Vacm* vacm = new Vacm(*mib);
reqList->set_vacm(vacm);
The Mib must contain the SNMP-TARGET-MIB, SNMP-NOTIFICATION-MIB and
usmUserTable (UsmUserTable) objects; the examples add them in init().
3.1 USM user (in init(), where the UsmUserTable is added to the Mib)
#include <agent_pp/v3_mib.h>
UsmUserTable *uut = new UsmUserTable(v3mp);
// Localize the keys for the local engine ID (needed for v3 TRAPs and
// for incoming requests of notifyUser) AND pass the passwords to the
// USM (this overload uses addPasswordsToUSM = true), so that SNMP++
// can localize the keys for the engine ID of any INFORM receiver it
// discovers later.
uut->addNewRow("notifyUser",
SNMP_AUTHPROTOCOL_HMAC192SHA256,
SNMP_PRIVPROTOCOL_AES128,
"notifyAuthPassword", "notifyPrivPassword");
mib.add(uut);
NOTE: The examples add their users with
uut->addNewRow("MD5", ..., "MD5UserAuthPassword", "", engineID, false);
i.e. localized for the local engine ID only, WITHOUT passwords in the
USM. Such users work for v3 TRAPs but not for INFORMs (see 2.3).
Alternative (b) - do not keep the passwords in the USM, but localize
the keys explicitly for a receiver whose engine ID is known in advance
(e.g. snmptrapd prints it at start-up or it is persisted in its
snmpapp.conf/snmptrapd.conf as "oldEngineID"):
OctetStr receiverEngineID(OctetStr::from_hex_string(
"80 00 1f 88 80 5a 3b 1c 2d 4e 6f 70 81 00 00 00 00"));
uut->addNewRow("notifyUser", "notifyUser",
SNMP_AUTHPROTOCOL_HMAC192SHA256,
SNMP_PRIVPROTOCOL_AES128,
"notifyAuthPassword", "notifyPrivPassword",
receiverEngineID,
false); // do not store the passwords in the USM
// Optional: tell the v3MP the engine ID of 192.0.2.10:162, so the
// first INFORM skips the unknown-engine-ID discovery round trip.
// This is required if discovery is disabled with
// v3mp->get_usm()->unset_discovery_mode(), because then a UTarget
// without engine ID is rejected for INFORMs.
v3mp->add_to_engine_id_table(receiverEngineID, "192.0.2.10", 162);
If the agent should also send v3 TRAPs, add the row for the local
engine ID as well (first addNewRow() above, or the engineID overload
with the local engine ID).
3.2 VACM: allow notifyUser to send the notifications
#include <agent_pp/vacm.h>
vacm->addNewGroup(SNMP_SECURITY_MODEL_USM, "notifyUser",
"notifyGroup", storageType_nonVolatile);
vacm->addNewAccessEntry("notifyGroup",
"", // context prefix
SNMP_SECURITY_MODEL_USM,
SNMP_SECURITY_LEVEL_AUTH_PRIV,
match_exact,
"", // no read view
"", // no write view
"notifyView", // notify view
storageType_nonVolatile);
// The notify view must contain the notification OID (e.g. coldStart
// 1.3.6.1.6.3.1.1.5.1) and the OIDs of all variable bindings.
vacm->addNewView("notifyView", "1.3.6.1", "",
view_included, storageType_nonVolatile);
The context ("" above) must match the context name passed to
generate()/notify(); the security level of the access entry must not
be higher than the one in snmpTargetParamsTable.
3.3 Notification targets
#include <agent_pp/notification_originator.h>
// Serialize an UDP address into the SNMP-TARGET-MIB TAddress format
// (IP address bytes followed by the 2 port bytes).
static OctetStr to_taddress(const UdpAddress& addr)
{
OctetStr taddress;
GenAddress gen(addr);
IpAddress ip(gen);
for (int i = 0; i < ip.get_length(); I++)
taddress += (unsigned char)ip[I];
taddress += (unsigned char)(addr.get_port() >> 8);
taddress += (unsigned char)(addr.get_port() & 0x00FF);
return taddress;
}
void init_notification_targets(NotificationOriginator& no)
{
UdpAddress receiver("192.0.2.10/162");
// (A) SNMPv3 TRAP (snmpNotifyType trap(1)).
// add_v3_trap_destination() creates the snmpTargetParamsEntry,
// the snmpNotifyEntry (type trap) and the snmpTargetAddrEntry,
// all with the row index "v3trapReceiver".
no.add_v3_trap_destination(receiver,
"v3trapReceiver", // row index
"v3trap", // tag
"notifyUser", // securityName
SNMP_SECURITY_LEVEL_AUTH_PRIV);
// (B) SNMPv3 INFORM (snmpNotifyType inform(2)).
// There is no add_v3_inform_destination(), the three rows are
// created directly. add_v3_trap_destination() cannot be used,
// since it always creates a trap(1) snmpNotifyEntry.
no.get_snmp_target_params_entry()->
add_entry("v3informParams", // row index
mpV3, // snmpTargetParamsMPModel
SNMP_SECURITY_MODEL_USM, // ...SecurityModel
"notifyUser", // ...SecurityName
SNMP_SECURITY_LEVEL_AUTH_PRIV); // ...SecurityLevel
no.get_snmp_notify_entry()->
add_entry("v3inform", // row index
"v3inform", // snmpNotifyTag
2); // snmpNotifyType inform(2)
MibTableRow* target = no.get_snmp_target_addr_entry()->
add_entry("v3informReceiver", // row index
NotificationOriginator::transport_domain_for(receiver),
to_taddress(receiver), // snmpTargetAddrTAddress
"v3inform", // snmpTargetAddrTagList
"v3informParams"); // snmpTargetAddrParams
// Optional: snmpTargetAddrTimeout (1/100 seconds, default 1500)
// and snmpTargetAddrRetryCount (default 3) are used for INFORMs.
if (target) {
target->get_nth(2)->replace_value(new SnmpInt32(500)); // 5 s
target->get_nth(3)->replace_value(new SnmpInt32(2)); // 2 retries
}
}
add_entry() returns 0 and leaves the existing row untouched if a row
with that index already exists, e.g. because it was restored from
persistent storage by mib->init(). So the code can safely run on every
start.
For illustration the same receiver is configured twice; it will receive
every notification once as TRAP and once as INFORM. In a real
configuration choose one of them per receiver. A target can also
carry several tags in its space separated snmpTargetAddrTagList.
3.4 Sending a notification
NotificationOriginator no(mib); // use the agent's Mib instance
init_notification_targets(no);
// coldStart without variable bindings
coldStartOid coldOid;
no.generate(0, 0, coldOid, "", "");
// or via the NotificationSender interface, with payload
Vbx vbs[1];
vbs[0].set_oid("1.3.6.1.2.1.1.5.0"); // sysName.0
vbs[0].set_value(OctetStr("myAgent"));
int status = no.notify("", // context
"1.3.6.1.4.1.4976.10.1.1.42", // notification OID
vbs, 1);
if (status != SNMP_ERROR_SUCCESS) {
// at least one notification could not be sent / was not
// acknowledged; see the "agent++.notification_originator" log
}
Instead of a local NotificationOriginator you can also register one
with the Mib (mib->set_notification_sender(...)) and send through
Mib::notify().
Sending is synchronous: for every INFORM target the call blocks until
the Response arrives or timeout * (retries + 1) has elapsed - plus the
discovery round trips for the first inform to a new receiver. Do not
send INFORMs from a time critical thread (for example while holding
locks on MIB objects needed for request processing); use a separate
thread if receivers may be unreachable.
====================================================================
4. TROUBLESHOOTING
------------------
- Nothing is sent, log "event did not pass notification filter":
check snmpNotifyFilterProfileTable/snmpNotifyFilterTable.
- Log "Notification not sent (reason) (addr) (params)" / "no access":
VACM denies notify access. Check group, access entry (context,
security model and level) and that the notify view includes the
notification OID and every variable binding OID.
- Log "target addr parameter row not found": the snmpTargetAddrParams
value of the target does not reference an active
snmpTargetParamsEntry.
- v3 TRAPs are not accepted by the receiver: the receiver's user is not
localized for the agent's engine ID (print it with
v3mp->get_local_engine_id().get_printable_hex()), or protocols /
passwords differ. Check the receiver's usmStatsUnknownEngineIDs,
usmStatsUnknownUserNames and usmStatsWrongDigests counters.
- v3 TRAPs work but INFORMs fail: the agent has no key for the
receiver's engine ID - add the user with addPasswordsToUSM = true
or a row localized for the receiver's engine ID (see 3.1). Also make
sure the receiver's user is localized for its OWN engine ID (no "-e"
with snmptrapd), and that discovery is enabled or the receiver's
engine ID has been added with v3MP::add_to_engine_id_table().
- NotificationOriginator::set_local_engine_id() does NOT influence USM
key localization. It only sets the engine ID used for the
notification log (NOTIFICATION-LOG-MIB) and for SNMP-COMMUNITY-MIB
lookups of v1/v2c targets. USM always uses the engine ID of the v3MP.