In Part 1 of this series (DNDJ, Vol. 1, issue 5), I described the steps
necessary to build a .NET interoperability library to communicate with the
Windows SNMP stack. In this article I will build on that foundation by
creating the necessary code to support an application capable of receiving
and displaying SNMP traps, commonly called an SNMP trap handler. As in Part
1, I will just touch on the more interesting parts of the code and leave you
to look at the code to get a complete picture. All of the code for both
articles can be downloaded from below.
What Is an SNMP Trap?
An SNMP trap is simply a notification message that is transmitted by an
SNMP-managed device whenever it has something of interest to report. Traps
can be thought of as event messages, similar to the events in the Windows
event logs. Traps, like regular SNMP variables, are defined in MIB
(Management Information Base) files. They are defined as a set of SNMP
variables contained in (or referenced by) an OID (Object Identifier). If you
look at the system directory on any Windows machine that has SNMP installed,
you will find several MIB files. (They have the extension ".mib".) If you
look through them you will see the variables that are supported on that
machine.
Why Are SNMP Traps Needed?
SNMP traps are the primary means of receiving notification of abnormal
(or normal) events from SNMP-enabled devices. This makes receiving and
interpreting the traps trap monitoring essential to the proper
management of a network. Usually, traps are sent to a management system
running software capable of receiving the traps, interpreting them, and
displaying notification messages in a graphical display. The people at the
management center viewing the display are then alerted to the problem. Most
management software also has the capability to send alerts to e-mail
accounts or pagers.
For simplicity, the trap handler I create here will receive SNMP traps
and display them on the console as text messages, but it would be relatively
easy to add some logic to interpret the trap and send e-mail or pager alerts
when specific conditions are detected.
WinSNMP and Traps
As SNMP has evolved, so has the format of the trap definitions. SNMP v1
and v2 trap formats are different, so they have to be handled in different
ways. As I mentioned in Part 1 of this series, WinSNMP supports both v1 and
v2 SNMP standards, and it is capable of translating v1 traps into v2 traps,
so all we have to do is deal with the SNMP v2 trap format.
Community Strings
One topic I didn't cover in Part 1 is SNMP community strings. Community
strings are an attempt to provide some form of security for SNMP. Each time
you communicate with an SNMP device you must pass it a community string,
which is just a sequence of ASCII characters; you can think of it as a group
name. When an SNMP agent receives a request, it tries to match the community
string in the request with one it has been configured to accept. If it finds
a match, it will proceed with the request. Community strings are also used
to restrict or allow write access by defining one community string for
read-only access and a different one for read-write access.
SNMP agents also use community strings to identify trap contexts, so an
application can look for traps with a specific community string.
Okay, let's get down to business!
Establishing a WinSNMP Session
The first step in any WinSNMP application is to create an SNMP session.
We can do this by calling SnmpCreateSession, which tells WinSNMP that our
application will need the resources to send and receive SNMP and messages.
SnmpCreateSession returns a session handle that we will use when we call
other WinSNMP functions.
IntPtr SnmpCreate
Session(IntPtr hwnd,
int msg,
SnmpCallback callback,
IntPtr data);
The key parameter here is the third, callback. This is defined as
SnmpCallback, which is defined in our library as a delegate. WinSNMP will
use this delegate to call back into our application each time it receives an
SNMP trap. To enable this, we create a SnmpCallback delegate and pass it to
this function.
SnmpAPI.SnmpCallback SnmpCB = new SnmpAPI.SnmpCallback(OnSnmpMessage);
OnSnmpMessage is the function that will be called with the trap
information. It is defined with parameters identical to the declaration of
the SnmpCallback delegate.
SNMPAPI_STATUS OnSnmpMessage(IntPtr session,
IntPtr hwnd,
int msg,
uint wparam,
uint lparam,
IntPtr data);
We create the delegate by instantiating a new variable of type
SnmpCallback and passing it a parameter reference to OnSnmpMessage.
SnmpCallback SnmpCB = new SnmpAPI.SnmpCallback(OnSnmpMessage);
Now we can pass the SnmpCB variable to SnmpCreateSession.
Registering to Receive Traps
Now that we have a WinSNMP session, the next thing we need to do to
receive trap information in our application is register it with the WinSNMP
API. This is done using the SnmpRegister function. In the SnmpAPI class
library in Part 1, we declared SnmpRegister as:
SNMPAPI_STATUS SnmpRegister(IntPtr session,
IntPtr src,
IntPtr dest,
IntPtr context,
IntPtr notification,
int state);
The parameters allow you to specify the source and destination address of
the traps you want to receive, and also to filter them by content. As we
will not be using these parameters, I will not go into great detail about
them. We will set them all to zero, which tells WinSNMP to send us all traps
from all sources.
Receiving the Traps
After the WinSNMP stack is configured, the application is ready to
receive traps. With the parameters we have specified, any trap that comes
into the computer will end up in our callback function, OnSnmpMessage. In
our application, OnSnmpMessage is responsible for receiving the trap
message, decoding it, and displaying it to the console. If you want to make
it more useful, it would not be much more work to make it take some other
action, such as sending an e-mail or page if it detects certain conditions.
The first step in decoding the trap information is to get it from
WinSNMP. This is done by calling SnmpRecvMsg.
SNMPAPI_STATUS rc = SnmpAPI.SnmpRecvMsg(session,
out src,
out dest,
out context,
out pdu);
SnmpRecvMsg returns the trap information in four out parameters.
Decoding the Trap Information
Now that we have the trap information, we need to convert it from the
SNMP representations into a format we can manipulate. Since we will be
displaying this data to the console, we will convert everything to strings.
The source and destination entities (src and dest) are really just IP
addresses. To convert them, we call SnmpEntityToStr.
SNMPAPI_STATUS rc = SnmpAPI.SnmpEntityToStr(dest, 1408, buffer);
string source Marshal.PtrToStringAnsi(buffer);
SNMPAPI_STATUS rc = SnmpAPI.SnmpEntityToStr(dest, 1408, buffer);
string source Marshal.PtrToStringAnsi(buffer);
SnmpEntityToStr returns a string representation of the entity reference
passed in. The returned string will be the IP address of the device sending
the trap (src) and the IP address of the device the trap was sent to (dest).
The buffer used in the above code is a work buffer. The creation of this
buffer is explained later.
Now we can move on to decoding the actual trap data. All SNMP data is
transmitted in what is called a protocol data unit (PDU). You can think of a
PDU as a container that holds the SNMP variables. In order to get at the
variables in the trap, we need to extract them from the PDU using
SnmpGetPduData function. SnmpGetPduData is declared as:
SnmpAPI.SnmpGetPduData(pdu,
out type, out id, out status,
out index, out vbl);
This function will decode the PDU and return the individual data components
in the out parameters.
type parameter: The type of SNMP message received.
id parameter: Will be set to the message id. SNMP is a connectionless protocol, and an application can send multiple messages to an SNMP device
before it receives a response. The application must use the id parameter to
match the responses to the messages it transmitted.
status and index parameters: Used to signal that there was an error in the corresponding SNMP request.
vbl parameter: A reference to the variable binding list (vbl). The vbl is just a list of SNMP variable names and their corresponding values.
Since we are dealing only with traps, we will ignore the id, status, and
index parameters. The vbl parameter is the most important, as it contains
all of the trap information sent by the SNMP device.
Displaying the Trap Information
Now that we have extracted the data components from the trap message
given to us by the WinSNMP stack, we can get on with the business of formatting and displaying the trap information to the console.
First, we must verify that the message we received is actually a trap.
In our case this will always be so because we are working only with trap
messages in this application. But in a more complicated SNMP application in
which you would be sending and receiving SNMP messages as well as traps, you
would need a way to tell the difference between a trap message and a message
from a device sent in response to an SNMP GET command.
The type parameter gives us this information, and we should check to see
that it is set to the value SNMPAPI_PDU.SNMP_PDU_TRAP. This tells us it is a
trap message.
After we have determined that we have a trap message, we can start to
display the trap information. First, we display a header message along with
the trap id.
Console.WriteLine("Trap received...");
Console.WriteLine("Id: " + id);
To display the SNMP variables in the vbl, we will need to do a bit more
work.
Decoding the Variable Binding List
In order to display the information in the variable binding list we
first need to get the number of variables that are in the vbl. For this we
use the SnmpCountVbl function. SnmpCountVbl takes the vbl as a parameter and
returns a count of the number of variables the vbl contains.
Second, we need to iterate over each variable in the vbl, translate it,
then write it to the console. To translate each variable, we need to call
SnmpGetVb and pass it the vbl and the index of the variable we are
interested in. It will return the name and value of the variable at that
position.
SMIOID name = new SMIOID();
SMIVALUE val = new SMIVALUE();
SNMPAPI_STATUS rc = SnmpAPI.SnmpGetVb
(vbl, index, ref name, ref val);
Note: The name and value parameters are passed as refs, so you need to
initialize them first.
The next step is to convert the variable name to a string. The variable
name is an OID, so we convert it by calling SnmpOidToStr. SnmpOidToStr takes
a reference to the OID and returns a string in the buffer specified by the
buffer parameter. We must allocate this buffer ourselves but how big does
it need to be?
If we look at the WinSNMP documentation, we see that the largest an OID
can be is 1408 bytes, so we allocate a buffer of this size. This might seem
like an odd size, but it's not. SNMP uses UDP (User Datagram Protocol) for
its transport protocol, and each SNMP message must fit into one UDP message.
The standard MTU (Maximum Transmission Unit) on most networks is 1500 bytes,
so, 1408 is what's left over when you subtract all the message headers
needed to get one variable across the network. We allocate this buffer by
calling the .NET marshaler.
IntPtr buffer = Marshal.AllocHGlobal(1408);
After we have the buffer, we can call SnmpOidToStr.
SNMPAPI_STATUS rc = SnmpAPI.SnmpOidToStr(ref oid, 1408, buffer);
This gives us a string in a global buffer allocated by the marshaler. But
all we have is an opaque internal pointer to a buffer. We must convert it to
a string type. For this, we call another .NET marshaler function, PtrToStringAnsi.
string str = Marshal.PtrToStringAnsi(buffer);
Now we have a string representation of the OID. Whew! All this work and
we still haven't decoded the data.
To convert the data to a displayable format, we need to first check the
type of data that was sent in the variable. Each variable value in SNMP
includes a type code to help determine the type of data the variable
contains. We will use this type to determine how to convert the data to a
string. Most of the conversions are straightforward, so I will refer you to
the function in Listing 1 that performs the conversions. Most of the
conversions are straightforward, but if you look at the string conversion
for the SNMP type OCTET_STRING, you will see that in order to determine
whether we have an ASCII string or a binary buffer we scan the buffer
looking for binary characters. If all the characters are ASCII, we make it a
string; If there are binary characters, we encode the buffer so it can be
displayed. I chose to unencode it because it was quicker to call the
ToBase64String function than to write a hex converter.
Running the Trap Handler
If you open the accompanying project in Visual Studio (I have VS.NET
2003, so you will have to make a new project and add the files if you have
an earlier version), and run SnmpTrapHandler, you will see the screen shown
in Figure 1.
Now you are ready to receive traps. An easy way to generate a couple of
traps is to stop and start the SNMP service on the machine where the trap
handler is running. Note that there are two SNMP services, one is called
"SNMP Service", and the other is "SNMP Trap Service". You will want to stop
"SNMP Service", then restart it. Once you've done this, you should see traps
appear in the trap handler console, as shown in Figure 2.
Conclusion
That's it. WinSNMP Traps 101. In the future, I will be writing more
about SNMP, including how to communicate with and manage real-world devices
such as Cisco network equipment.
About The Author
Jim Thomas is a telecommunications and networking consultant and the operating
manager at Bocacom.net LLC, a dedicated server provider located in Boca Raton, FL. In addition to writing, he spends his spare time developing network management applications
for the .NET Framework.
jimt@bocacom.net
Listing 1
protected string VbToString(ref SMIVALUE sval)
{
string val = "";
//
// Convert all types to a string value
//
switch (sval.type) {
//
// Standard 32 bit integer conversion
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_INT:
val = sval.val.sNumber.ToString();
break;
//
// Standard 32 bit unsigned integer conversion
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_UINT32:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_CNTR32:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_GAUGE32:
val = sval.val.uNumber.ToString();
break;
//
// Timeticks are in hundredths of a second (.01). We need to mul-
// tiply by ten to send ms (.001) to TimeSpan.
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_TIMETICKS:
val = TimeSpan.FromMilliseconds((long)sval.val.uNumber*10)
.ToString();
val = val.Substring(0, val.Length-4);
break;
//
// Standard 64 bit integer conversion
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_CNTR64:
val = sval.val.hNumber.ToString();
break;
//
// Byte array conversion. SNMP doesn't differentiate between
// ASCII and binary data, so we perform a check ourselves and
// convert binary data to ASCII (using ToBase64String, which is
// uuencode).
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_BITS:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_OPAQUE:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_OCTETS:
if (sval.val.str.size > 0) {
int i = 0;
byte[] bits = new byte[sval.val.str.size];
Marshal.Copy(sval.val.str.octets, bits, 0,
(int)sval.val.str.size);
for (i = 0; i < bits.Length; i++) {
if (char.IsControl((char)bits[i]) &&
(i < bits.Length-1 || bits[i] != 0)) {
val = Convert.ToBase64String(bits, 0, bits.Length);
break;
}
}
if (i == bits.Length)
val = Marshal.PtrToStringAnsi(sval.val.str.octets,
(int)sval.val.str.size);
SnmpAPI.SnmpFreeDescriptor
((int)sval.type, ref sval.val.str);
}
break;
//
// Ip Address conversion
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_NSAPADDR:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_IPADDR:
IPAddress addr = new IPAddress((long)(UInt32)Marshal
.ReadIntPtr(sval.val.str.octets));
val = addr.ToString();
SnmpAPI.SnmpFreeDescriptor((int)sval.type, ref sval.val.str);
break;
//
// SNMP OID conversion
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_OID:
val = OidToString(ref sval.val.oid);
break;
//
// Catch the rest
//
case SNMPAPI_SYNTAX.SNMP_SYNTAX_NULL:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_NOSUCHOBJECT:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_NOSUCHINSTANCE:
case SNMPAPI_SYNTAX.SNMP_SYNTAX_ENDOFMIBVIEW:
val = "(null)";
break;
}
return(val);
}
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com