SNMP Version 3 Low Level Packet Class

SNMP operations for version 3 are inherited from the protocol version 2. SNMP version 3 supports Get, GetNext, GetBulk, Set, Response, V2Trap and Inform requests, responses and notifications.

New features in SNMP version 3 offer new security functions not available in previous versions of the protocol.

Security features include:

  • Message timeliness
  • Message authentication
  • Message privacy or encryption

Additional to the above security features, additional security functionality is available, for example VACL, which are not covered in this document but are supported by the SnmpSharpNet library.

This document assumes that you are familiar with concepts outlined in the SNMP Version 3 Concepts page and is focused on manager application development.

The information you are going to need for every SNMP version 3 request, regardless of the security model used, is:

  • Authoritative SNMP EngineID
  • Authoritative SNMP EngineBoots
  • Authoritative SNMP EngineTime
  • Secret (user) name

Information above is the minimum information required to construct a valid noAuthNoPriv (no authentication and no privacy) request.

Minimum required information is stored in the User Security Model header. To better understand the packet format and information required to construct a valid request here is the packet format for SNMP version 3:

Field # Name Data type Description
1 Packet sequence Sequence First sequence wraps the entire SNMP packet.
2 Version Integer32 SNMP Protocol version number
3 Global data Sequence Global message data sequence
4 Message ID Integer32 Message id
5 MaxMsgSize Integer32 Maximum message size accepted by the host
6 MsgFlags OctetString Single byte message flags.
7 SecModel Integer32 Security model used in the message. USM = 3
– End of Global data sequence –
8 AuthEngineID OctetString Authoritative SNMP EngineID
9 AuthEngineBoots Integer32 Authoritative SNMP Engine boots value
10 AuthEngineTime Integer32 Authoritative SNMP Engine time value
11 Secret (user) name OctetString Secret name
12 AuthParams OctetString Message authentication parameters
13 PrivParams OctetString Message privacy parameters
14 ScopedPdu Sequence SNMP version 3 scoped Pdu sequence
15 ContextEngineID OctetString Context engine ID
16 ContextName OctetString Context name
17 Pdu Sequence Standard Pdu compatible with SNMP v1 and v2
18 RequestID Integer32 Unique request ID (not related to MessageID)
19 ErrorStatus Integer32 Error status code
20 ErrorIndex Integer32 Reference to the Vb entry in the Variable Bindings that caused the error
21 VarBinds Sequence Sequence around all Vb entries
22 Vb Sequence Vb sequence Oid/Value
23 Oid Oid Vb Oid
24 AsnValue various ASN.1 value associated with the Vb Oid
– End of Vb — Multiple Vb entries can be part of the VarBinds sequence
– End of Var Binds –
– Packet sequence –

As you can see, SNMP version 3 packet is considerably more complex then its predecessors.

Now lets figure out what needs to be done to build a basic noAuthNoPriv request.

Before you can make requests, you need to have the SecretName (username) for the hosts you wish to query. Think of this as the equivalent of the SNMP community name in SNMP v1 and v2.

Security name is not enough. You will need authoritative agent engine information before you can make requests. To get this information, you will need to send a discovery request.

Discovery request is a request to the authoritative SNMP engine with AuthEngineID, AuthEngineBoots, AuthEngineTime, SecurityName, ContextEngineID and ContextName fields set to null (or 0 for Integer32 fields). Message flags has to have the Reportable bit set to 1 (true) to notify the SNMP agent that we wish to receive failure notifications (called Reports in SNMP v3 terminology).

To construct a discovery packet do the following:

// Create discovery packet using SnmpV3Packet.DiscoveryRequest() static method
SnmpV3Packet discovery = SnmpV3Packet.DiscoveryRequest();
// BER encode discovery packet
byte[] outBuffer = discovery.encode();
// Send discovery packet to the agent
mysocket.SendTo(outBuffer, new IPEndPoint(agentIP, agentPort));

When reply is received (assuming received data is stored in inBuffer byte array and length of received data is stored in the inLength variable) following will verify discovery procedure was successful:

// Decode received packet
discovery.decode(inBuffer, inLength);
// Make sure received packet is SNMP v3 Report
if( discovery.Version != SnmpVersion.Ver3 )
{
	return; // Invalid SNMP version
}
if( discovery.Pdu.Type != PduType.Report )
{
	return; // Invalid response. We have to get a Report
}
if( ! discovery.Pdu.VbList[0].Oid.Equals(SnmpConstants.usmStatsUnknownEngineIDs) )
{
	return; // Wrong kind of error message sent by agent
}
// Success !!

Returned Report from the SNMPv3 agent will contain authoritative EngineID, EngineBoots and EngineTime values. Timeliness check that is performed on every SNMPv3 packet is tied to EngineBoots and EngineTime. If request received by the agent contains SnmpBoots value that is not equal to the value on the Agent, request is rejected. Also, if SnmpTime is not within 150 seconds of the value on the agent, request is rejected.

There is not much you can do with the EngineBoots value. This value can be incremented because Agent was rebooted, SNMP stack on the agent was restarted or because EngineTime has reached maximum value and had to roll back to 0 (which is when SnmpBoots value has to be incremented by the agent. Because there are a lot of events that can cause EngineBoots value to change, there is not much point in worrying about it. Just store the value and reuse it in following requests.

EngineTime on the other hand is a value that you can track and increment according to the time that has elapsed between the discovery process and the time you are sending you request. Just storing the discovery time in a DateTime class will enable you to calculate the elapsed time.

You will know that time values (EngineBoots and EngineTime) are out of sync with the Agent if you receive a Report with Oid usmStatsNotInTimeWindows in the VarBinds. If this happens, you will need to perform an additional discovery to update the timeliness values.

Another handy value to retain is MaxMessageSize. This value is the maximum length (in bytes) that the Agent can process in a single request. You should make sure not to send requests to the agent that will exceed this size.

So let’s store discovered values:

// Authoritative Engine ID
OctetString engineID = (OctetString)discovery.USM.EngineId.Clone();
// Authoritative Engine Boots
Int32 engineBoots = discovery.USM.EngineBoots;
// Authoritative Engine Time
Int32 engineTime  = discovery.USM.EngineTime;
// Timestamp when discovery process was completed
DateTime discoveryTime = DateTime.Now;
// Maximum message size agent can process
Int32 maxMessageSize = discovery.MaxMessageSize;

With this information you are ready to construct a request. Here is how you do it:

// Request packet class
SnmpV3Packet request = new SnmpV3Packet();
// Set security model to NoAuthNoPriv with SecurityName "milan"
request.NoAuthNoPriv(ASCIIEncoding.UTF8.GetBytes("milan"));
// Set authoritative engine ID retrieved during discovery
request.SetEngineId(engineID);
// Set timeliness values retrieved during discovery
request.SetEngineTime(engineBoots, engineTime);
// Set maximum message size
request.MaxMessageSize = maxMessageSize;
// Set Pdu type to Get
request.ScopedPdu.Type = PduType.Get;
// Add Oid to query to the VbList
request.ScopedPdu.VbList.Add(".1.3.6.1.2.1.1.1.0");
// Encode request
outBuffer = request.encode();

// Now you are ready to send the request to the agent

Before going any further, you should know what request.NoAuthNoPriv(byte[]) method does. This is just a helper method that sets flags (in SnmpV3Packet.MsgFlags) for Authentication and Privacy to false and sets the SecurityName (or user name) to the value specified. Comparable methods are available for authNoPriv and authPriv security modes that make generation of appropriately secured packets as easy as possible.

After sending the request and receiving a raw reply, you can parse it in two ways. One way is to reuse the packet class you used to send the request like this:

request.decode(inBuffer, inLength);

And data is decoded correctly. You can do the same with a new packet class when making noAuthNoPriv requests, like this:

// Create a response packet class
SnmpV3Packet response = new SnmpV3Packet();
// Decode response
response.decode(inBuffer, inLength);
 
// Process response
Console.WriteLine("Reply received to message id {0} request id {1}", 
             response.MessageId, response.Pdu.RequestId);
Console.WriteLine("\t{0}: {1} {2}", response.ScopedPdu.VbList[0].Oid.ToString(),
  SnmpConstants.GetTypeName(response.ScopedPdu.VbList[0].Value.Type),
  response.ScopedPdu.VbList[0].Value.ToString());

Introducing additional layers of security does not complicate coding too much. For example, to change the above example to support authNoPriv security mode, you would do the following:

request.authNoPriv(ASCIIEncoding.UTF8.GetBytes("milan"),
	ASCIIEncoding.UTF8.GetBytes("myAuthSecret"), AuthenticationDigests.MD5);

When decoding reply information from the agent, you will need to initialize SnmpV3Packet class with the correct authentication information for the verification of the received information to succeed. Reply from the agent processing will look like this:

// Create a response packet class
SnmpV3Packet response = new SnmpV3Packet();
// Set authentication parameters to use with the incoming packet
response.authNoPriv(ASCIIEncoding.UTF8.GetBytes("milan"),
	ASCIIEncoding.UTF8.GetBytes("myAuthSecret"), AuthenticationDigests.MD5);
// Decode response
response.decode(inBuffer, inLength);

If haven't noticed, there is a potential issue with the need to preset the authentication information in the packet prior to parsing received packets. In a situation where you send multiple requests to multiple agents, you will not know which security mode to apply to packets as they are coming back (assuming each agent or request could have a different mode). This is addressed with SnmpV3Packet.GetUSM(byte[],int) method.

GetUSM() method allows you to "look ahead" into the received byte buffer and parse only the header information, up to and including User Security Model header information. When you call this method, information that will be parsed will include authoritative SNMP engine id, security name, and authentication and privacy flags that you can then map with localy stored security parameters and fill in authentication and privacy secrets needed for packet decoding and verification. We'll get back to how to use this feature later.

First lets look at how to process authPriv authentication and privacy encrypted packets. This is done in a similar way to adding authentication, you will use a helper method authPriv to set security parameters related to authentication and privacy. Remeber, according to the SNMP standard, you cannot use privacy without authentication. Here is what needs to be changed in the original example to introduce privacy:

request.authPriv(ASCIIEncoding.UTF8.GetBytes("milan"),
	ASCIIEncoding.UTF8.GetBytes("myAuthSecret"), AuthenticationDigests.MD5,
	ASCIIEncoding.UTF8.GetBytes("myPrivSecret"), PrivacyProtocols.DES);

In this example, we are specifying authentication protocol as DES with the myPrivSecret secret (encryption password). When you call SnmpV3Packet.encode() method, packet with the above security configuration will have the ScopedPdu portion of the packet encrypted and then MD5 authenticated for security.

Decoding a response to an authPriv request requires security settings to be set correctly in the SnmpV3Packet class prior to decoding. Here is an example that will decode responses to the above configured requests:

// Create a response packet class
SnmpV3Packet response = new SnmpV3Packet();
// Set authentication and privacy parameters to use with the incoming packet
request.authPriv(ASCIIEncoding.UTF8.GetBytes("milan"),
	ASCIIEncoding.UTF8.GetBytes("myAuthSecret"), AuthenticationDigests.MD5,
	ASCIIEncoding.UTF8.GetBytes("myPrivSecret"), PrivacyProtocols.DES);
// Decode response
response.decode(inBuffer, inLength);

When preparing requests, you have to make sure that security name, authentication digest and secret and privacy protocol and secret match what is configured on the agent you wish to request information from.

Available AuthenticationDigests are MD5 and SHA1 and PrivacyProtocols are DES, TripleDES, AES-128, AES-192 and AES-256. Any combination of authentication digests and privacy protocols can be configured.