SNMP version 3 notifications (traps and informs)

SNMP version 3 supports two types of notifications, traps and informs. Traps are identical to version 2 traps and are unacknowledged notifications sent by agents to managers. Informs are acknowledged notifications. Agent sends an inform notification and waits for acknowledgment. If acknowledgment is not received within the configured timeout period, inform notification is re-sent until a reply is received or maximum retry value has been reached.

Both trap and inform notifications support noAuthNoPriv, authNoPriv and authPriv security models but handle them in a different way.

Traps are generated by the agent and authoritative SNMP engine for a trap packet is the sending SNMP agent. Since generator of the message and authoritative engine are one and the same, there is no need for the SNMPv3 discovery process.

Inform are generated by the agent but authoritative SNMP engine is the manager (receiver). That means that sender of the inform notification has to perform SNMPv3 discovery to retrieve engine id (which some agents can be pre-configured with), boots and time values it requires to generate a valid inform notification packet.

To be able to work with informs, SNMP manager application needs to maintain local engine id, boots and time values that it can send to the agents when a SNMPv3 Discovery request is received. For processing of valid notifications, a list of security names (user names), authentication and privacy settings and related secrets need to be maintained for correct authentication and, if used, decryption of received notifications.

For trap handling, list of agent engine ids, security names, authentication and privacy settings and related secrets are needed.

It will be clearer what is needed in the examples below…

We’ll begin with traps since they are easier to process. Trap uses agents (trap sender) engine id, boots and time values so all we need is to keep a list of configuration values that will allow us to process the packet.

Here is an example of agent settings entry:

public class USMPeer
{
	protected OctetString _engineId;
	protected OctetString _securityName;
	protected AuthenticationDigests _authenticationType;
	protected PrivacyProtocols _privacyType;
	protected OctetString _authenticationSecret;
	protected OctetString _privacySecret;
	public USMPeer()
	{
		_engineId = new OctetString();
		_securityName = new OctetString();
		_authenticationType = AuthenticationDigests.None;
		_authenticationSecret = new OctetString();
		_privacyType = PrivacyProtocols.None;
		_privacySecret = new OctetString();
	}
	public USMPeer(OctetString engineId, OctetString securityName)
		: this()
	{
		_engineId.Set(engineId);
		_securityName.Set(securityName);
	}
	public USMPeer(OctetString engineId, OctetString securityName,
		AuthenticationDigests authDigest, OctetString authSecret)
		: this(engineId, securityName)
	{
		_authenticationType = authDigest;
		_authenticationSecret.Set(authSecret);
	}
	public USMPeer(OctetString engineId, OctetString securityName,
		AuthenticationDigests authDigest, OctetString authSecret, 
		PrivacyProtocols privType, OctetString privSecret)
		: this(engineId, securityName, authDigest, authSecret)
	{
		_privacyType = privType;
		_privacySecret.Set(privSecret);
	}
	public bool IsMatch(OctetString engineId, OctetString securityname)
	{
		if (_engineId.Equals(engineId) && _securityName.Equals(securityname))
			return true;
		return false;
	}
	public bool IsMatch(OctetString securityname)
	{
		if (_engineId.Length <= 0 && _securityName.Equals(securityname))
			return true;
		return false;
	}
	public SnmpSharpNet.OctetString EngineId
	{
		get { return _engineId; }
	}
	public SnmpSharpNet.OctetString SecurityName
	{
		get { return _securityName; }
	}
	public SnmpSharpNet.AuthenticationDigests AuthenticationType
	{
		get { return _authenticationType; }
		set { _authenticationType = value; }
	}
	public SnmpSharpNet.PrivacyProtocols PrivacyType
	{
		get { return _privacyType; }
		set { _privacyType = value; }
	}
	public SnmpSharpNet.OctetString AuthenticationSecret
	{
		get { return _authenticationSecret; }
	}
	public SnmpSharpNet.OctetString PrivacySecret
	{
		get { return _privacySecret; }
	}
}

When creating a peer information storage classes, it is a good idea to allow for a generic entry that doesn’t depend on authoritative engine id but is selected based on SecurityName. This way you’ll be able to have a group of devices all configured with the same security name, authentication and privacy parameters and not have to store and manage a separate entry for each one. I have done this by allowing EngineId to be length 0 meaning it doesn’t matter what the engine id is.

Now that you have a class to store required values, create a collection to store them and populate it with agent values:

// Collection to hold agent information
public List<USMPeer> _usmPeers =  new List<USMPeer>();
 
// noAuthNoPriv agent with security name test
_usmPeers.Add(new USMPeer(null, new OctetString("test")));
 
// authNoPriv agent using MD5 authentication with security name testMD5 and
//  authentication secret md5authNoPriv
_usmPeers.Add(new USMPeer(null, new OctetString("testMD5"), AuthenticationDigests.MD5,
	new OctetString("md5authNoPriv")));
 
// authNoPriv agent using SHA-1 authentication with security name testSHA1 and
//  authentication secret sha1authNoPriv
_usmPeers.Add(new USMPeer(null, new OctetString("testSHA1"), AuthenticationDigests.SHA1, 
	new OctetString("sha1authNoPriv")));
 
// authPriv agent using MD5 authentication and DES privacy with security name testDESMD5,
//  authentication secret md5desauthPriv and privacy secret md5desAuthPriv
_usmPeers.Add(new USMPeer(null, new OctetString("testDESMD5"), AuthenticationDigests.MD5, 
	new OctetString("md5desauthPriv"), PrivacyProtocols.DES, new OctetString("md5desAuthPriv")));
 
// authPriv agent using SHA-1 authentication and DES privacy with security name testDESMD5,
//  authentication secret sha1desauthPriv and privacy secret sha1desauthPriv
_usmPeers.Add(new USMPeer(null, new OctetString("testDESMD5"), AuthenticationDigests.SHA1, 
	new OctetString("sha1desauthPriv"), PrivacyProtocols.DES, new OctetString("sha1desAuthPriv")));
 
// authPriv agent using MD5 authentication and AES-128 privacy with security name testAESMD5,
//  authentication secret md5aesAuthPriv and privacy secret md5aesAuthPriv
_usmPeers.Add(new USMPeer(null, new OctetString("testAESMD5"), AuthenticationDigests.MD5, 
	new OctetString("md5aesauthPriv"), PrivacyProtocols.AES128, new OctetString("md5aesAuthPriv")));
 
// authPriv agent using SHA-1 authentication and AES-128 privacy with security name testAESSHA1,
//  authentication secret sha1aesAuthPriv and privacy secret sha1aesAuthPriv
_usmPeers.Add(new USMPeer(null, new OctetString("testAESSHA1"), AuthenticationDigests.SHA1, 
	new OctetString("sha1aesauthPriv"), PrivacyProtocols.AES128, new OctetString("sha1aesAuthPriv")));

Above agent entries are not tied to a specific authoritative SNMP engine (or agent). To create an agent specific entry do the following:

// noAuthNoPriv agent with security name test tied to specific engine id
_usmPeers.Add(new USMPeer(
     new OctetString(new byte[] { 0x04,0x0D,0x80,0x00,0x1F,0x88,0x80,0x9A,0x4A,0x00,0x00,0x2A,0xC0,0x9A,0x49}, 
     new OctetString("test")));

You’ll probably need a lookup routine to make it easy to find entries:

public USMPeer FindPeer(OctetString engineId, OctetString securityName)
{
	// look for specific entires first trying to match both engineId and securityName
	if( engineId != null )
	{
		foreach (USMPeer peer in _usmPeers)
		{
			if (peer.IsMatch(engineId, securityName))
				return peer;
		}
	}
	// no specific entries found. Look for entries not assigned to specific engine id
	foreach (USMPeer peer in _usmPeers)
	{
		if (peer.IsMatch(securityName))
			return peer;
	}
 
	return null; // match not found
}

Now you can store and retrieve information you need about peers and can start working with traps.

There are no shortcuts in the library to handling the communications side of things. You will need to create a receiving socket, bind it and wait for packets. One way to do this is:

// We'll need a byte buffer to store incoming data
byte[] inbuffer = new byte[32 * 1024];
// End point details of the host we received packet(s) from
EndPoing peer = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
// Create a IP/UDP socket
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// Bind the socket to the standard snmptrapd port = udp/162
socket.Bind((EndPoint)new IPEndPoint(IPAddress.Any, 162));
// Wait for a packet
int inlen = _socket.ReceiveFrom(inbuffer, SocketFlags.None, ref peer);
// Make sure we received some data instead of an empty packet.
if( inlen &lt;= 0 )
{
	Console.WriteLine("Received an invalid SNMP packet length 0 Bytes.");
	socket.Close();
	return;
}

To be able to fully process a packet, we need to get the authoritative engine id and security name from the packet we received. SnmpV3Packet.decode function is not suited for this purpose because it attempts to parse, authenticate and un-encrypt the packet which is not possible before we set the authentication and privacy parameters.

To get a peek at the information inside the packet without performing a full parse, you use SnmpV3Packet.GetUSM method:

// Make sure received packet is SNMP version 3
int ver = SnmpPacket.GetProtocolVersion(_inbuffer, inlen);
 
if( ver != (int)SnmpVersion.Ver3 )
{
	Console.WriteLine("Received packet is not SNMP version 3.");
	socket.Close();
	return;
}
 
SnmpV3Packet packet = new SnmpV3Packet();
 
// Do partial parse to get enough information to set authentication and privacy info
UserSecurityModel usm = packet.GetUSM(_inbuffer, inlen);
 
// Check this is a valid packet
 
// Valid engine id has to be set in the packet.
if( usm.EngineId.Length &lt;= 0 )
{
	Console.WriteLine("Invalid packet. Authoritative engine id is not set.");
	_socket.Close();
	return;
}
// Security name has to be set in a valid packet
if( usm.SecurityName.Length &lt;= 0 )
{
	Console.WriteLine("Invalid packet. Security name is not set.");
	_socket.Close();
	return;
}

With engine id and security name information we can lookup security parameters that apply to this packet using the above defined FindPeer() method and apply the found values to the SnmpV3Packet class to enable data validation and decoding:

// Locate the peer entry
USMPeer peer = FindPeer(usm.EngineId, usm.SecurityName);
if (peer == null)
{
	// Couldn't find the peer so abort further processing...
	Console.WriteLine("SNMP packet from unknown peer.");
	socket.Close();
	return;
}
 
// Set authentication type for the peer
packet.USM.Authentication = peer.AuthenticationType;
// Set privacy type from the peer
packet.USM.Privacy = peer.PrivacyType;
// If privacy is used, set the privacy secret in the packet class.
if( peer.PrivacyType != PrivacyProtocols.None )
	packet.USM.PrivacySecret.Set(peer.PrivacySecret);
// If authentication is used, set the authentication secret in the packet class.
if( peer.AuthenticationType != AuthenticationDigests.None )
	packet.USM.AuthenticationSecret.Set(peer.AuthenticationSecret);
// Decode the packet.
packet.decode(_inbuffer, inlen);

This is a Trap packet demonstration so we want to make sure we received the correct packet type:

if (packet.Pdu.Type != PduType.V2Trap)
{
	Console.WriteLine("Invalid SNMP version 3 packet type received.");
}

Now that you’ve decoded the packet, what do you do with it?

Format of the V2TRAP packet is identical to any other SNMPv3 packet type. Specific requirements for the V2TRAP packets are that first OID in the VbList has to be trapSysUpTime.0 and second OID in the VbList has to be trapObjectID.0. When decoding the V2TRAP packet types, in both SNMP v2 and v3 classes, attempt is made to locate these two values and store them in the special variables, away from VbList, for easier access. To keep things simple, parser expects the two variables in question to be in the exactly prescribed positions. That means if trapSysUpTime.0 variable is not the first variable in VbList, it will not receive special treatment. Same is true for trapObjectID.0. If it is not the second variable in the VbList no special processing will take place.

Once parsed, you can use mandated trapSysUpTime.0 and trapObjectID.0 values:

Console.WriteLine("trapSysUpTime.0: {0}", packet.Pdu.TrapSysUpTime.ToString());
Console.WriteLine("trapObjectID.0 : {0}", packet.Pdu.TrapObjectID.ToString());

From here, access the VbList to get more information about the trap you received:

foreach( Vb v in packet.Pdu.VbList )
{
	Console.WriteLine("{0}: {1}", v.Oid.ToString(), v.Value.ToString());
}

That’s all there is to trap reception and processing with SNMPv3.

Informs are a little more complex but not impossible :)

The two major differences between traps and informs in SNMP version 3 is that informs are acknowledged and that you (the manager application) are the authoritative SNMP engine.

What does this mean to you? It means you have to use a local engine id, keep track of the engine time and respond to discovery requests.

This sounds like a lot of work just to receive notifications, and I happen to agree, but that’s the way it is and there are plenty of helper methods in the library to make this as easy as possible.

To begin, define you local engine parameters so you can deal with discovery requests:

// Define an engine id
OctetString engineId = 
     new OctetString(new byte[] { 0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, 0x00 });
// Store time value at the time your application started
DateTime engineTime = DateTime.Now;

In this example, we’ll stick with engine boots value of 0. I still haven’t run into a peer that couldn’t or wouldn’t handle it and it is so much easier then keeping track of how many times the application was started and stopped.

When you receive a packet, you should check if it is a discovery request:

SnmpV3Packet packet = new SnmpV3Packet();
// Have a look inside the packet for security information
UserSecurityModel usm = packet.GetUSM(inbuffer, inlen);
// It's a discovery request if engine id or security name are empty values
if (usm.EngineId.Length &lt;= 0 || usm.SecurityName.Length &lt;= 0)
{
	// Calculate application uptime
	int dateTime = Convert.ToInt32(Math.Floor(DateTime.Now.Subtract(engineTime).TotalMinutes));
	// Use SnmpV3Packet.DiscoveryResponse() method to create a discovery operation reply packet
	SnmpV3Packet discoveryResponse = 
	    SnmpV3Packet.DiscoveryResponse(packet.MessageId, 0, _engineId, 0, dateTime, 1);
	// BER encode the reply packet
	byte[] outPacket = discoveryResponse.encode();
	// and send the encoded reply back to the originator of the request.
	socket.SendTo(outPacket, ep);
}

If you looked closely in the code above, SnmpV3Packet.DiscoveryResponse() method is called with packet.MessageId. This was one of those coding accidents that, for once, worked out. We need the messageId value from the received packet but the way I designed the GetUSM() method, we only get the USM header back. Lucky, to get to the USM header inside the received SNMP packet, SnmpV3Packet class parses the packet up to that point, including the messageId value which is then stored in the class internal variable for us to retrieve.

While I am sure there are many ways to make this a lot better, prettier, etc. I’m not going to mess with it. It works the way it is.

So now what happens?, you are asking. Well, since we (or you) are in control, authentication and privacy for remote hosts is managed by SecurityName only. This is obviously the case since we are the authoritative engine id and all inform notifications arriving will have the same engine id.

Now you can use FindPeer() method (defined above) to find the peer security settings:

// We now received another packet after discovery process was completed...
 
SnmpV3Packet packet = new SnmpV3Packet();
// Have a look inside the packet for security information
UserSecurityModel usm = packet.GetUSM(inbuffer, inlen);
// Find SNMP peer (remember, engineId value is your application sent value
USMPeer peer = FindPeer(usm.EngineId, usm.SecurityName);
if (peer == null)
{
	// Couldn't find the peer so abort further processing...
	Console.WriteLine("SNMP packet from unknown peer.");
	socket.Close();
	return;
}
 
// Set authentication type for the peer
packet.USM.Authentication = peer.AuthenticationType;
// Set privacy type from the peer
packet.USM.Privacy = peer.PrivacyType;
// If privacy is used, set the privacy secret in the packet class.
if( peer.PrivacyType != PrivacyProtocols.None )
	packet.USM.PrivacySecret.Set(peer.PrivacySecret);
// If authentication is used, set the authentication secret in the packet class.
if( peer.AuthenticationType != AuthenticationDigests.None )
	packet.USM.AuthenticationSecret.Set(peer.AuthenticationSecret);
// Decode the packet.
packet.decode(_inbuffer, inlen);

We are not done yet. Inform notification sender expects an ack packet. This is no problem at all. Helper method SnmpV3Packet.BuildInformResponse() method will fill in all relevant information and format a new packet class appropriately to be sent as an ack for the received inform notification:

SnmpV3Packet infResponse = SnmpV3Packet.BuildInformResponse(packet);
byte[] outPkt = infResponse.encode();
socket.SendTo(outPkt, ep);

Please note that maximum message size handling has not been implemented in Inform response messages. This will cause issues with agent handling of responses to very large inform notifications. Fix is planned in the next bug fix release.