Getting Started with IPWorks SNMP 2016

Requirements: IPWorks SNMP

Introduction

IPWorks SNMP is a comprehensive toolkit for building secure network management solutions. It supports a complete range of network management capabilities including advanced SNMPv3 security features, trap handling, MIB parsing, and more.

The toolkit consists of agent, manager, and trap manager components for both UDP-based and TCP-based applications, as well as an MIB browser:

  • SNMPAgent
  • SNMPMgr
  • SNMPTrapMgr
  • SNMPTCPAgent
  • SNMPTCPMgr
  • SNMPTCPTrapMgr
  • MibBrowser
Note: This article uses the UDP-based component names for the sake of brevity, but you always have the option of using the TCP-based components instead. See Code Snippet Notes at the bottom of the page for assumptions about code snippets used in this article.

Contents


SNMP Overview

SNMP, or Simple Network Management Protocol, was designed to provide a standard method of collecting data from, and controlling, managed network devices. The components in this toolkit support all three versions of the SNMP protocol in use today: v1, v2c (default), and v3. The SNMPVersion property can be set to specify the version of SNMP to use.

In an SNMP network, devices are either agents or managers; and there are typically few managers, and many agents. SNMP agents are responsible for gathering and keeping track of data, and for responding to SNMP managers. SNMP managers are responsible for polling agents for data, and can also request that an agent change the value of an object. SNMP managers also receive and acknowledge traps sent by SNMP agents. (In IPWorks SNMP, the SNMPTrapMgr component is specifically designed to handle traps, though the SNMPMgr component has limited trap-handling capabilities as well.)

MIBs (Management Information Base)

SNMP agents store data in a Management Information Base, or MIB. The MIB is, put simply, a hierarchical structure shaped much like a tree. Every object in the tree has a number and a string which are unique to that level of the hierarchy, as well as a value. An Object Identifier, or OID, is a string which refers to a specific object in the MIB. SNMP agents use the MIB hierarchy to store data by associating values with OIDs.

Vendors often add custom branches to an agent's MIB with their own objects and values. However, much of the MIB is predefined as a global standard, and this article uses predefined OIDs for the purpose of simplicity.

SNMP Commands

All operations in SNMP are accomplished using a handful of commands. More details and usage examples for each command are provided in later sections, however below is a brief description of supported commands (PDUs):

  • GetRequest: Sent from a manager to an agent to get the value of one or more variables.
  • GetNextRequest: Sent from a manager to an agent to request that, for one or more variables, the agent reply with the lexicographically next variable(s) and associated value(s).
  • GetBulkRequest: Similar to doing multiple GetNextRequests, a manager sends this to an agent to request that, for one or more variables, the agent reply with multiple lexicographically next variables and associated values. (This command is only present in SNMP v2c and up.)
  • SetRequest: Sent from a manager to an agent to set the value of one or more variables.
  • Response: Sent from an agent to a manager to respond to any of the above requests.
  • Trap: Sent from an agent to a manager to notify it of a significant event.
  • InformRequest: An asynchronous notification with acknowledgement that is typically used for manager-to-manager communication.
For any request an agent receives, it must retrieve (and potentially update) all of the desired data atomically before sending a response.

Basic GetRequests and SetRequests

The SNMPAgent and SNMPMgr components make it easy to send and handle basic Get and Set requests. For example, the code snippets below would result in the following output (from the SNMPMgr code):

Getting current sysContact and sysName values from MyAgent
Response from MyAgent:
OID="1.3.6.1.2.1.1.4"; Value="Contact Email: person@fake.com"
OID="1.3.6.1.2.1.1.5"; Value="device1"

Setting new sysName value for MyAgent
Response from MyAgent:
OID="1.3.6.1.2.1.1.5"; Value="device2"

SNMPMgr Code (C#)
// Response event handler.
snmpmgr1.OnResponse += (s, e) => {
    Console.WriteLine("Response from " + e.SourceAddress + ":");
    foreach (SNMPObject obj in snmpmgr1.Objects)
        Console.WriteLine("OID=\"" + obj.Oid + "\"; Value=\"" + obj.Value + "\"");
};

snmpmgr1.RemoteHost = "MyAgent";
Console.WriteLine("Getting current sysContact and sysName values from " + snmpmgr1.RemoteHost);
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.4"));
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.5"));
snmpmgr1.SendGetRequest();

snmpmgr1.Objects.Clear();
Console.WriteLine("\nSetting new sysName value for " + snmpmgr1.RemoteHost);
snmpmgr1.Objects.Add(new SNMPObject {
    Oid = "1.3.6.1.2.1.1.5",
    Value = "device2",
    ObjectType = SNMPObjectTypes.otOctetString
});
snmpmgr1.SendSetRequest();
SNMPAgent Code (C#)See Code Snippet Notes
// GetRequest event handler.
snmpagent1.OnGetRequest += (s, e) => {
    // Copy objects from request, then clear Objects collection so we can build response.
    List<SNMPObject> requestObjects = new List<SNMPObject>(snmpagent1.Objects);
    snmpagent1.Objects.Clear();

    // Get requested objects with values.
    foreach (SNMPObject requestObj in requestObjects) {
        SNMPObject dataObj = get(requestObj.Oid);
        if (dataObj != null) snmpagent1.Objects.Add(dataObj);
    }

    // Send response.
    e.Respond = true;
};

// SetRequest event handler.
snmpagent1.OnSetRequest += (s, e) => {
    // Update stored data with values from objects in request.
    foreach (SNMPObject obj in snmpagent1.Objects) {
        SNMPObject dataObj = get(obj.Oid);
        if (dataObj != null) dataObj.Value = obj.Value
    }

    // Send response back using same objects as sent in request.
    e.Respond = true;
};

Note: The above code snippets make no attempt to handle error cases as defined by SNMP, such as a nonexistent OID, an invalid data type (when setting), etc.

Sending and Responding to GetNextRequests

The SNMPAgent and SNMPMgr components also make it easy to send and handle GetNext requests. For example, the code snippets below would result in the following output (from the SNMPMgr code):

Getting next objects after sysContact and sysName from MyAgent
Response from MyAgent:
OID="1.3.6.1.2.1.1.5"; Value="device1"
OID="1.3.6.1.2.1.1.6"; Value="location"
SNMPMgr Code (C#)
// Response event handler.
snmpmgr1.OnResponse += (s, e) => {
    Console.WriteLine("Response from " + e.SourceAddress + ":");
    foreach (SNMPObject obj in snmpmgr1.Objects)
        Console.WriteLine("OID=\"" + obj.Oid + "\"; Value=\"" + obj.Value + "\"");
};

snmpmgr1.RemoteHost = "MyAgent";
Console.WriteLine("Getting next objects after sysContact and sysName from " + snmpmgr1.RemoteHost);
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.4"));
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.5"));
snmpmgr1.SendGetNextRequest();
SNMPAgent Code (C#)See Code Snippet Notes
// GetNext event handler.
snmpagent1.OnGetNext += (s, e) => {
    // Copy objects from request, then clear Objects collection so we can build response.
    List<SNMPObject> requestObjects = new List<SNMPObject>(snmpagent1.Objects);
    snmpagent1.Objects.Clear();

    // Loop over objects from request.
    foreach (SNMPObject requestObj in requestObjects) {
        // Add the first object which comes after this response object's OID to the list.
        SNMPObject next = getNext(requestObj.Oid);
        if (next != null) snmpagent1.Objects.Add(next);
    }

    // Send the response.
    e.Respond = true;
};

Sending and Responding to GetBulkRequests

When using SNMP version 2c or higher, the SNMPMgr component can make use of the SendGetBulkRequest() method in order to avoid repeated calls to SendGetNextRequest.

The result set of a call to SendGetBulkRequest() is controlled by two parameters: nonRepeaters and maxRepetitions. To help explain what effect these parameters have when calling SendGetBulkRequest(), I will first provide a couple of code snippets and the output that would result.

SNMPMgr Code (C#)
snmpmgr1.Objects.Clear();
// Specify some OIDs that we are interested in getting information about.
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.1"));
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.2"));
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.3"));
snmpmgr1.Objects.Add(new SNMPObject("1.3.6.1.2.1.1.4"));

// Send the GetBulk request to MyAgent with nonRepeaters=2 and maxRepetitions=3.
snmpmgr1.RemoteHost = "MyAgent";
snmpmgr1.SendGetBulkRequest(2, 3);

// Print the returned OIDs
foreach (SNMPObject obj in snmpmgr1.Objects) {
    Console.WriteLine(obj.Oid);
}
SNMPAgent Code (C#)See Code Snippet Notes
// GetBulkRequest event handler.
snmpagent1.OnGetBulkRequest += (s, e) => {
    // Copy objects from request, then clear Objects collection so we can build response.
    List<SNMPObject> requestObjects = new List<SNMPObject>(snmpagent1.Objects);
    snmpagent1.Objects.Clear();

    // Loop over objects from request.
    for (int i = 0; i < requestObjects.Count; i++) {
        if (i < e.NonRepeaters) {
            // Add the first object which comes after this response object's OID to the list.
            SNMPObject next = getNext(requestObjects[i].Oid);
            if (next != null) snmpagent1.Objects.Add(next);
        } else {
            // Do a "GetNext" [MaxRepetitions] times for this request object.
            for (int j = 0; j < e.MaxRepetitions; j++) {
                SNMPObject next = getNext(requestObjects[i + j].Oid);
                if (next != null) snmpagent1.Objects.Add(next);
            }
        }
    }

    // Send the response.
    e.Respond = true;
};

As you can see, I've included four OIDs with my request, 1.3.6.1.2.1.1.1-1.3.6.1.2.1.1.4. I've also set nonRepeaters to 2 and maxRepetitions to 3. The output of this code snippet would be:

1.3.6.1.2.1.1.2
1.3.6.1.2.1.1.3
1.3.6.1.2.1.1.4
1.3.6.1.2.1.1.5
1.3.6.1.2.1.1.6
1.3.6.1.2.1.1.5
1.3.6.1.2.1.1.6
1.3.6.1.2.1.1.7

The number of objects returned in the response may vary based on packet size constraints, but it will never be more than N + (M * R), where N is the value of nonRepeaters, M is the value of maxRepetitions, and R is the number OIDs in the request minus N (so in our example, N=2, M=3, and R=2). Now, let's discuss why we got the results we did.

The effect of nonRepeaters

By setting nonRepeaters to 2, I requested that the agent do a single GetNext for the first two OIDs I provided (1.3.6.1.2.1.1.1 and 1.3.6.1.2.1.1.2). The following OIDs are the result of that:

1.3.6.1.2.1.1.2
1.3.6.1.2.1.1.3

The effect of maxRepetitions

By setting maxRepetitions to 3, I've requested that the agent then continually do a GetNext, up to three times, for the remaining OIDs (1.3.6.1.2.1.1.3 and 1.3.6.1.2.1.1.4). The following OIDs are the result of that:

1.3.6.1.2.1.1.4 (First GetNext originating from 1.3.6.1.2.1.1.3)
1.3.6.1.2.1.1.5 (Second GetNext originating from 1.3.6.1.2.1.1.3)
1.3.6.1.2.1.1.6 (Third GetNext originating from 1.3.6.1.2.1.1.3)
1.3.6.1.2.1.1.5 (First GetNext originating from 1.3.6.1.2.1.1.4)
1.3.6.1.2.1.1.6 (Second GetNext originating from 1.3.6.1.2.1.1.4)
1.3.6.1.2.1.1.7 (Third GetNext originating from 1.3.6.1.2.1.1.4)

SNMP Table Walking

An SNMP walk, or "table walk" is just a series of GetNext requests. The requests start at the beginning OID for a particular table (i.e., the Interfaces table; 1.3.6.1.2.1.2), and continue on until the response from the agent is outside of the table. With the SNMPMgr component, you can easily do an SNMP table walk by calling the Walk() method and passing the desired tableOid. (You can also set the WalkLimit property to a non-zero value if you wish to limit the number of objects the walk will traverse in the table, but we won't do that in the demo below.)

The code snippet below for SNMPMgr demonstrates how to perform a table walk of the aforementioned Interfaces table, as well as how to use the MibBrowser component to help make the output more user-friendly. The code snippet below that one, for SNMPAgent, shows how to handle the GetNext requests that will occur as the Interfaces table is walked.

SNMPMgr Code (C#)Download SNMPMibs.zip
// Load the IF-MIB file, which contains the Interfaces table definition. You can get that Mib file,
// and others, by clicking the "Download SNMPMibs.zip" link at the top right of this code snippet.
mibbrowser1.LoadMib("IF-MIB");
// We'll want the MibBrowser to be lenient with us when we call SelectNode()...more on this later.
mibbrowser1.ExactMatch = false;

// If this isn't true, then you must capture the objects each time the Response event fires, which
// will happen multiple times during the walk.
snmpmgr1.StoreWalkObjects = true;
snmpmgr1.RemoteHost = "MyAgent";
snmpmgr1.Walk("1.3.6.1.2.1.2"); // Walk the iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable table.

// Loop over the objects that we got back from the table walk and print out their information.
// We'll use the MibBrowser component to help us print out more user-friendly information.
foreach (SNMPObject obj in snmpmgr1.Objects) {
    // Have the MibBrowser select the node corresponding to our object's OID. Note that since
    // we set ExactMatch to false, it will just traverse the tree as far as possible, then stop.
    // This is helpful since the MIB won't contain OIDs that include the trailing index (.#)
    // (See the SNMPAgent code snippet below for information about indices work for SNMP tables).
    mibbrowser1.SelectNode(obj.Oid);
    
    string value = obj.Value;
    // Some columns' values should be handled in a special way.
    if (mibbrowser1.NodeLabel.Contains("ifPhysAddress")) {
        // Translate the ValueB bytes to the MAC address format we're used to.
        value = "";
        foreach (byte b in obj.ValueB) value += b.ToString("X2") + ":";
        if (value.Length > 0) value = value.Substring(0, value.Length - 1);
    } else if (mibbrowser1.NodeLabel.Contains("ifOperStatus")) {
        // Translate the values for the .ifEntry.ifOperStatus column to a user-friendly name.
        // (You'll need to import the "System.Net.NetworkInformation" namespace for this line.)
        OperationalStatus opStatus = (OperationalStatus) Convert.ToInt32(obj.Value);
        value = opStatus.ToString();
    }
    
    // Write the information for this object.
    Console.WriteLine(mibbrowser1.NodeFullLabel.Replace("iso.org.dod.internet.mgmt.mib-2", "") + 
        " (" + obj.Oid + ") = " + value);
}
SNMPAgent Code (C#)
// At some point outside of your GetNextRequest event handler, get a list of the system's network
// interfaces. In this demo, we're using the "System.Net.NetworkInformation" namespace's classes 
// for this, but you can do it another way if you want to.
NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();

// GetNextRequest event handler.
snmpagent1.OnGetNextRequest += (s, e) => {
    // For the sake of brevity, we'll only handle _some_ of the 22 possible columns in the
    // iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable table.

    // Get a reference to the first object in the request (we'll assume there's only one for the demo).
    SNMPObject obj = snmpagent1.Objects[0];
    
    if (obj.Oid.Equals("1.3.6.1.2.1.2") || // iso.org.dod.internet.mgmt.mib-2.interfaces
        obj.Oid.Equals("1.3.6.1.2.1.2.1")) // .interfaces.ifNumber
    {
        // Respond with .ifNumber.0, whose value is the number of rows in the table.
        // (.0 is because the object is scalar, not columnar)
        obj.Oid = "1.3.6.1.2.1.2.1.0";
        obj.ObjectType = SNMPObjectTypes.otInteger;
        obj.Value = networkInterfaces.Length.ToString();
    }
    else if (obj.Oid.Equals("1.3.6.1.2.1.2.1.0") || // .interfaces.ifNumber.0
             obj.Oid.Equals("1.3.6.1.2.1.2.2") ||   // .interfaces.ifTable
             obj.Oid.Equals("1.3.6.1.2.1.2.2.1") || // .interfaces.ifTable.ifEntry
             obj.Oid.Equals("1.3.6.1.2.1.2.2.1.1")) // .interfaces.ifTable.ifEntry.ifIndex
    {
        // Respond with .ifIndex.1. The .ifIndex column simply holds the index numbers for the 
        // rows of the table, so their values will always be the same as the actual index used
        // in the OID.
        // (.1 because the object is columnar, and this object will hold the value for the first
        // row's instance of the column.)
        obj.Oid = "1.3.6.1.2.1.2.2.1.1.1";
        obj.ObjectType = SNMPObjectTypes.otInteger;
        obj.Value = "1"; // This is a special case, we're always returning the first row's index.
    }
    else if (obj.Oid.StartsWith("1.3.6.1.2.1.2.2.1.1.")) //.ifEntry.ifIndex.#
    {
        // Since the above block always handles when the OID is exactly .ifIndex, we know the OID
        // for this block must be "1.3.6.1.2.1.2.2.1.1.#" (.ifIndex.#), where # is the row number
        // we're interested in. So let's extract that row number from the OID first...
        int row = Convert.ToInt32(obj.Oid.Replace("1.3.6.1.2.1.2.2.1.1.", ""));
        
        // Now, since this is a GetNext request, we need to respond with the _next_ row number, as
        // long as we have another "row"...i.e., as long as there's another element in the
        // networkInterfaces array. (Keep in mind that SNMP row indexes are 1-based!)
        if (networkInterfaces.Length != row) {
            // There's another network interface, so respond with .ifIndex.[# + 1].
            obj.Oid = "1.3.6.1.2.1.2.2.1.1." + (row + 1);
            obj.ObjectType = SNMPObjectTypes.otInteger;
            obj.Value = (row + 1).ToString();
        } else {
            // There isn't another network interface, so respond with .ifEntry.ifDescr.1 instead.
            obj.Oid = "1.3.6.1.2.1.2.2.1.2.1";
            obj.ObjectType = SNMPObjectTypes.otOctetString;
            obj.Value = networkInterfaces[0].Description;
        }
    }
    else if (obj.Oid.StartsWith("1.3.6.1.2.1.2.2.1.2.")) // .ifEntry.ifDescr.#
    {
        // From here on, we're essentially doing the same thing as the block above, we're just
        // handling different columns in each one (and, as you'll see from later blocks, skipping
        // some columns for the purpose of the demo).
        int row = Convert.ToInt32(obj.Oid.Replace("1.3.6.1.2.1.2.2.1.2.", ""));
        
        if (networkInterfaces.Length != row) {
            obj.Oid = "1.3.6.1.2.1.2.2.1.2." + (row + 1);
            obj.ObjectType = SNMPObjectTypes.otOctetString;
            obj.Value = networkInterfaces[row].Description;
        } else {
            // Next column we'll handle is .ifEntry.ifSpeed (1.3.6.1.2.1.2.2.1.5), so respond with the
            // first row for that column; .ifEntry.ifSpeed.1
            obj.Oid = "1.3.6.1.2.1.2.2.1.5.1";
            obj.ObjectType = SNMPObjectTypes.otGauge32;
            // Note that the Gauge type has a max value of 4294967295.
            obj.Value = networkInterfaces[0].Speed < 4294967295 ? networkInterfaces[0].Speed.ToString() : "4294967295";
        }
    }
    else if (obj.Oid.StartsWith("1.3.6.1.2.1.2.2.1.5.")) // .ifEntry.ifSpeed.#
    {
        int row = Convert.ToInt32(obj.Oid.Replace("1.3.6.1.2.1.2.2.1.5.", ""));
        
        if (networkInterfaces.Length != row) {
            obj.Oid = "1.3.6.1.2.1.2.2.1.5." + (row + 1);
            obj.ObjectType = SNMPObjectTypes.otGauge32;
            obj.Value = networkInterfaces[row].Speed < 4294967295 ? networkInterfaces[0].Speed.ToString() : "4294967295";
        } else {
            // Next column we'll handle is .ifEntry.ifPhysAddress (1.3.6.1.2.1.2.2.1.6), so respond with the
            // first row for that column; .ifEntry.ifPhysAddress.1
            obj.Oid = "1.3.6.1.2.1.2.2.1.6.1";
            obj.ObjectType = SNMPObjectTypes.otOctetString;
            obj.ValueB = networkInterfaces[0].GetPhysicalAddress().GetAddressBytes();
        }
    }
    else if (obj.Oid.StartsWith("1.3.6.1.2.1.2.2.1.6.")) // .ifEntry.ifPhysAddress.#
    {
        int row = Convert.ToInt32(obj.Oid.Replace("1.3.6.1.2.1.2.2.1.6.", ""));
        
        if (networkInterfaces.Length != row) {
            obj.Oid = "1.3.6.1.2.1.2.2.1.6." + (row + 1);
            obj.ObjectType = SNMPObjectTypes.otOctetString;
            obj.ValueB = networkInterfaces[row].GetPhysicalAddress().GetAddressBytes();
        } else {
            // Next column we'll handle is .ifEntry.ifOperStatus (1.3.6.1.2.1.2.2.1.8), so respond with the
            // first row for that column; .ifEntry.ifOperStatus.1
            obj.Oid = "1.3.6.1.2.1.2.2.1.8.1";
            obj.ObjectType = SNMPObjectTypes.otInteger;
            obj.Value = ((int)networkInterfaces[0].OperationalStatus).ToString();
        }
    }
    else if (obj.Oid.StartsWith("1.3.6.1.2.1.2.2.1.8.")) // .ifEntry.ifOperStatus.#
    {
        int row = Convert.ToInt32(obj.Oid.Replace("1.3.6.1.2.1.2.2.1.8.", ""));
        
        if (networkInterfaces.Length != row) {
            obj.Oid = "1.3.6.1.2.1.2.2.1.8." + (row + 1);
            obj.ObjectType = SNMPObjectTypes.otInteger;
            obj.Value = ((int) networkInterfaces[row].OperationalStatus).ToString();
        } else {
            // For the purpose of this demo, we aren't handling any other columns, so we just need to
            // return something that's outside of the table. We'll use 1.3.6.1.2.1.11.2
            // (iso.org.dod.internet.mgmt.mib-2.snmp.snmpOutPkts) and pass the RequestId as the value.
            obj.Oid = "1.3.6.1.2.1.11.2";
            obj.ObjectType = SNMPObjectTypes.otCounter32;
            obj.Value = snmpagent1.RequestId.ToString();
        }
    } else {
        // For the purpose of this demo, we aren't handling any other columns. (If you're using the
        // SNMPMgr code above, you won't hit this block, don't worry.)
        e.ErrorStatus = 2;  //No such name
    }

    // Send the response.
    e.Respond = true;
};

The exact output of this demo will look different depending on what network interfaces your machine has, but it should look similar to the following one. Notice a few things here:

  • What we've been colloquially referring to as the .interfaces "table" (1.3.6.1.2.1.2) is not actually an SNMP table, but rather an SNMP group which contains two things: a scalar object .ifNumber, and a table object .ifTable.
  • The table object .ifTable contains multiple instances of the columnar object .ifEntry, which can be thought of as a "row".
  • The columnar object .ifEntry, being a "row", contains multiple scalar objects, such as .ifIndex, ifDescr, etc.
  • The "row" index comes after the column number in an OID. This is why the table's objects are listed column-by-column when you do a walk, rather than row-by-row.

.interfaces.ifNumber.0 (1.3.6.1.2.1.2.1.0) = 6
.interfaces.ifTable.ifEntry.ifIndex.1 (1.3.6.1.2.1.2.2.1.1.1) = 1
.interfaces.ifTable.ifEntry.ifIndex.2 (1.3.6.1.2.1.2.2.1.1.2) = 2
.interfaces.ifTable.ifEntry.ifIndex.3 (1.3.6.1.2.1.2.2.1.1.3) = 3
.interfaces.ifTable.ifEntry.ifIndex.4 (1.3.6.1.2.1.2.2.1.1.4) = 4
.interfaces.ifTable.ifEntry.ifIndex.5 (1.3.6.1.2.1.2.2.1.1.5) = 5
.interfaces.ifTable.ifEntry.ifIndex.6 (1.3.6.1.2.1.2.2.1.1.6) = 6
.interfaces.ifTable.ifEntry.ifDescr.1 (1.3.6.1.2.1.2.2.1.2.1) = Hyper-V Virtual Ethernet Adapter #2
.interfaces.ifTable.ifEntry.ifDescr.2 (1.3.6.1.2.1.2.2.1.2.2) = Npcap Loopback Adapter
.interfaces.ifTable.ifEntry.ifDescr.3 (1.3.6.1.2.1.2.2.1.2.3) = Hyper-V Virtual Ethernet Adapter
.interfaces.ifTable.ifEntry.ifDescr.4 (1.3.6.1.2.1.2.2.1.2.4) = Microsoft Wi-Fi Direct Virtual Adapter
.interfaces.ifTable.ifEntry.ifDescr.5 (1.3.6.1.2.1.2.2.1.2.5) = Bluetooth Device (Personal Area Network)
.interfaces.ifTable.ifEntry.ifDescr.6 (1.3.6.1.2.1.2.2.1.2.6) = Software Loopback Interface 1
.interfaces.ifTable.ifEntry.ifSpeed.1 (1.3.6.1.2.1.2.2.1.5.1) = 4294967295
.interfaces.ifTable.ifEntry.ifSpeed.2 (1.3.6.1.2.1.2.2.1.5.2) = 1410065408
.interfaces.ifTable.ifEntry.ifSpeed.3 (1.3.6.1.2.1.2.2.1.5.3) = 1410065408
.interfaces.ifTable.ifEntry.ifSpeed.4 (1.3.6.1.2.1.2.2.1.5.4) = 1410065408
.interfaces.ifTable.ifEntry.ifSpeed.5 (1.3.6.1.2.1.2.2.1.5.5) = 1410065408
.interfaces.ifTable.ifEntry.ifSpeed.6 (1.3.6.1.2.1.2.2.1.5.6) = 1410065408
.interfaces.ifTable.ifEntry.ifPhysAddress.1 (1.3.6.1.2.1.2.2.1.6.1) = 02:00:4C:4F:4F:50
.interfaces.ifTable.ifEntry.ifPhysAddress.2 (1.3.6.1.2.1.2.2.1.6.2) = 02:00:4C:4F:4F:50
.interfaces.ifTable.ifEntry.ifPhysAddress.3 (1.3.6.1.2.1.2.2.1.6.3) = F4:8E:38:AF:59:ED
.interfaces.ifTable.ifEntry.ifPhysAddress.4 (1.3.6.1.2.1.2.2.1.6.4) = 16:53:30:3D:71:A9
.interfaces.ifTable.ifEntry.ifPhysAddress.5 (1.3.6.1.2.1.2.2.1.6.5) = 94:53:30:3D:71:AA
.interfaces.ifTable.ifEntry.ifPhysAddress.6 (1.3.6.1.2.1.2.2.1.6.6) = 
.interfaces.ifTable.ifEntry.ifOperStatus.1 (1.3.6.1.2.1.2.2.1.8.1) = Down
.interfaces.ifTable.ifEntry.ifOperStatus.2 (1.3.6.1.2.1.2.2.1.8.2) = Up
.interfaces.ifTable.ifEntry.ifOperStatus.3 (1.3.6.1.2.1.2.2.1.8.3) = Up
.interfaces.ifTable.ifEntry.ifOperStatus.4 (1.3.6.1.2.1.2.2.1.8.4) = Down
.interfaces.ifTable.ifEntry.ifOperStatus.5 (1.3.6.1.2.1.2.2.1.8.5) = Down
.interfaces.ifTable.ifEntry.ifOperStatus.6 (1.3.6.1.2.1.2.2.1.8.6) = Up

For the purpose of the demo we don't handle all of the columns that the Interfaces table has. Nonetheless, it's easy to see how the SNMPAgent component can be used to respond to GetNext requests when walking a table, and how the SNMPMgr and MibBrowser components can be used in conjunction to make the results of the table walk more user-friendly.

Note that a "walk" is not to be confused with a GetBulk request; the former is not a true SNMP command, it just uses repeated GetNext requests behind the scenes. The GetBulk request, on the other hand, is a true SNMP command and is more efficient than doing repeated GetNext commands. However, the Walk() method is still useful when you want to walk a table, or when using SNMPv1 (which has no support for the GetBulk command).

Traps

The SNMPAgent component makes it easy to send traps through the use of the SendTrap() method, and the SNMPTrapMgr component is designed to simplify the task of handling traps. The code snippets below show how to send and handle one of the standard SNMP traps and produce the following output (from the SNMPTrapMgr code):

Trap with OID=1.3.6.1.6.3.1.1.5.1 received from MyAgent on port 162.
SNMPAgent Code (C#)
// For standard traps, SNMPAgent recognizes both human-readable trap names
// and the full trap OIDs (which for "coldStart" is "1.3.6.1.6.3.1.1.5.1").
snmpagent1.SendTrap("MyTrapMgr", "coldStart");
SNMPTrapMgr Code (C#)
// Trap event handler.
snmptrapmgr1.OnTrap += (s, e) => {
    Console.WriteLine("Trap with OID=" + e.TrapOID + " received from " + e.SourceAddress + 
        " on port " + e.SourcePort + ".");
};

The following table shows the standard SNMP traps:

Trap Name Full Trap OID Generic Type
coldStart 1.3.6.1.6.3.1.1.5.1 0
warmStart 1.3.6.1.6.3.1.1.5.2 1
linkDown 1.3.6.1.6.3.1.1.5.3 2
linkUp 1.3.6.1.6.3.1.1.5.4 3
authenticationFailure 1.3.6.1.6.3.1.1.5.5 4
egpNeighborLoss 1.3.6.1.6.3.1.1.5.6 5
enterpriseSpecific 1.3.6.1.6.3.1.1.5.7 6

Enterprise-Specific Traps

To send an enterprise specific trap using SNMPAgent, you must first set the TrapEnterprise configuration setting. By default, it has a value of 1.3.6.1.6.3.1.1.5 (i.e., SNMPv2-MIB::snmpTraps). After you set the TrapEnterprise setting, you set the trapOID parameter of the SendTrap() method based on the kind of trap you want to send.

If you simply want to send one of the generic SNMP traps, you can set trapOID to one of the trap name strings or full trap OIDs from the table above. If you want to send an enterprise-specific trap, you can set trapOID to a string that follows the format "6.x", where x is the enterprise-specific trap type (6 is the generic type that indicates an enterprise-specific trap). For example, if you wanted to send an enterprise-specific trap of type 5000:

SNMPAgent Code (C#)
snmpagent1.Config("TrapEnterprise=1.3.6.1.4.1.311");
snmpagent1.SendTrap("MyTrapMgr", "6.5000");

Using Custom Objects in Traps

When sending a trap with SNMPAgent, there may be some cases where you want to use your own custom objects. If you wish to do this, you are responsible for including the sysUpTime and snmpTrapOid objects before your own objects (the component handles this automatically for built-in traps).

SNMPAgent Code (C#)
snmpagent1.Objects.Clear();
//Add the sysUpTime object.
snmpagent1.Objects.Add(new SNMPObject {
    Oid = "1.3.6.1.2.1.1.3.0",
    Value = snmpagent1.SysUpTime.ToString(),
    ObjectType = SNMPObjectTypes.otTimeTicks
});

//Add the snmpTrapOid object.
snmpagent1.Objects.Add(new SNMPObject {
    Oid = "1.3.6.1.6.3.1.1.4.1.0",
    Value = "YourTrapOID",
    ObjectType = SNMPObjectTypes.otObjectId
});

//Now you can add your own objects.
snmpagent1.Objects.Add(new SNMPObject {
    Oid = "YourObjectOID",
    Value = "Your Value",
    ObjectType = SNMPObjectTypes.otOctetString
});

snmpagent1.SendTrap("MyTrapMgr", "ignored");

NOTE: The trapOID value passed to the SendTrap() method is ignored in favor of the trap OID value you explicitly specify in the snmpTrapOid object.

SNMPv3

SNMP version 3 added a number of new features, one of the most important being the user security model (USM), which lets you authenticate and encrypt messages. There are a number of important concepts to know in order to understand the SNMPv3 USM, so here's a quick summary:

  • User: A user has a name, a security level, and an algorithm and password for authentication and (based on the security level) privacy.
  • Security Level: The SNMPv3 spec defines three security levels: None, authentication only, and authentication plus privacy. These are the only valid combinations; when using
  • Authentication: SNMPv3 authenticates messages by signing them using a user's authentication password and algorithm. The following authentication algorithms are available:
    • 1 - HMAC-MD5-96
    • 2 - HMAC-SHA-96
    • 3 - HMAC-192-SHA-256
  • Privacy/Encryption: If applicable, SNMPv3 encrypts messages using a user's encryption password and algorithm. The following encryption algorithms are available:
    • 1 - DES (default)
    • 2 - AES
    • 3 - 3DES
    • 4 - AES192
    • 5 - AES256

When using the SNMPv3 USM, all requests sent by a manager (or traps sent by an agent) will be signed, and optionally encrypted, based on a user's security level. The agent and trap manager components provide a couple of ways to handle SNMPv3 authentication when requests and traps (respectively) are received. You can use the AddUser() method (please see the AddUser() method's documentation for more details) or you can use events, which is the approach that will be covered in this section.

Sending a secure request or trap using the SNMPMgr or SNMPAgent components requires that you specify some common pieces of information. For SNMPMgr, this is done using properties; for SNMPAgent, it's done using parameters on the SendSecureTrap() method. This table summarizes this:

What Data SNMPMgr Property Name SNMPAgent Parameter Name
User User user
Authentication Algorithm AuthenticationProtocol authenticationProtocol
Authentication Password AuthenticationPassword authenticationPassword
Encryption Algorithm EncryptionAlgorithm encryptionAlgorithm
Encryption Password EncryptionPassword encryptionPassword

NOTE: Due to how the USM works, when sending secure requests using the SNMPMgr component, you'll also need to set the RemoteEngineId, RemoteEngineBoots, and RemoteEngineTime properties. You can do this manually, or, you can use the Discover() method to automatically discover the correct values for the current RemoteHost. See the following section, Discovery, for more information.

Discovery

When using SNMP version 3, it is required that SNMPMgr discover certain information about an agent before trying to send other requests to it. As part of the discovery process, the SNMPMgr will also authenticate itself to the agent through the standard USM authentication process. Successful discovery of an agent will cause the following properties to be set:

  • RemoteEngineId
  • RemoteEngineBoots
  • RemoteEngineTime (the component will handle keeping this value up to date after discovering it)
This code snippet shows how to initiate the discovery process using SNMPMgr:

SNMPMgr Code (C#)
snmpmgr1.RemoteHost = "MyAgent";
snmpmgr1.User = "user";
snmpmgr1.AuthenticationProtocol = 1; // Default, HMAC-MD5-96.
snmpmgr1.AuthenticationPassword = "authpass";
snmpmgr1.EncryptionAlgorithm = 1; // Default, DES.
snmpmgr1.EncryptionPassword = "encryptpass";
snmpmgr1.Discover();

The SNMPAgent.OnDiscoveryRequest event handler already has its Respond parameter set to true by default, so you typically don't need to handle that event explicitly. However, you will need to handle the SNMPv3 USM authentication process; refer to the Handling SNMPv3 Messages section for more details.

Secure Traps

When using SNMP version 3, the SNMPAgent can send a secure trap by using the SendSecureTrap() method. Unlike sending secure requests, there is no need to discover information about the receiver of the trap before sending it. This code snippet shows how to send a secure trap using SNMPAgent:

SNMPAgent Code (C#)
// For standard traps, SNMPAgent recognizes both human-readable trap names
// and the full trap OIDs (which for "coldStart" is "1.3.6.1.6.3.1.1.5.1").
snmpagent1.SendSecureTrap("MyTrapMgr", "coldStart", "user", 1, "authpass", 1, "encryptpass");

When using the SNMPTrapMgr component, you handle the secure trap itself in the same manner as a non-secure trap; refer to the Traps section for more information. The difference is that you also need to handle the SNMPv3 USM authentication process; refer to the Handling SNMPv3 Messages section for more details.

Handling SNMPv3 Messages

Processing an SNMPv3 message requires a bit of extra work to handle because these messages are secured by the USM. This applies whether you are handling secure requests (including discovery requests) with SNMPAgent, or handling secure traps with SNMPTrapMgr.

These components have an internal authentication cache which stores user authentication information. When a secure message is received, the component will first check the user cache. If the applicable user's information is already in the cache*, the component will handle the USM authentication process for you, and you'll be free to handle the request/trap as usual. If the user's information isn't in the cache, the event-based USM authentication process is initiated.

If you're using SNMPv3 security, you must handle secure requests/traps by managing the user cache correctly, or by handling the GetUserSecurityLevel and GetUserPassword events correctly. More information about each method is provided in the sections that follow.

For simpler use cases, using the user cache will likely be a better choice as the component will automatically handle most of the work. Use cases which need more control over the USM authentication process than the user cache allows will likely be better suited to the event-based method. You can also use a combination of the two, if you prefer.

*If a user is in the cache, but the cached information isn't correct, the BadPacket event will be fired. Please refer to the documentation for more details.

User Cache Method

Interacting with the user authentication cache is done using the following methods:

  • AddUser() method: Adds a user, along with their authentication and encryption information, to the cache.
  • ShowCache() method: Causes the CacheEntry event to be fired for every cache entry.
  • ClearCache() method: Clears the cache of all data.
This code snippet shows how to use these methods:

C#
// Add a user who uses authentication and encryption (security level 2).
snmpagent1.AddUser("user", 1, "authpass", 1, "encryptpass");
// Add a user who uses authentication only (security level 1).
snmpagent1.AddUser("user2", 1, "authpass2", 1, "");

// CacheEntry event handler.
snmpagent1.OnCacheEntry += (s, e) => {
    Console.WriteLine("User=" + e.User + "; AuthAlg=" + e.AuthenticationProtocol + 
        "; AuthPass=" + AuthenticationPassword + "; EncAlg=" + e.EncryptionAlgorithm +
        "; EncPass=" + EncryptionPassword);
};
// Print out the cache entries.
snmpagent1.ShowCache();

// Clear the cache.
snmpagent1.ClearCache();

Event-Based Method

The event-based method of handling the USM authentication process consists of, at most, the following event sequence occurring. More details about how to handle each of these events is provided in the subsections that follow.

  1. The GetUserSecurityLevel event fires
  2. The GetUserPassword event fires
  3. The GetUserPassword event fires again

In order to respond to the GetUserSecurityLevel and GetUserPassword events correctly, you will need to maintain the appropriate security credentials for each user. This includes the appropriate security level for that user as well as the passwords for authentication and, optionally, encryption.

For the code examples in this section I am storing an example user's credentials in a hash table using tuples, as shown in the following code snippet (note that you can use any method you prefer, the way you store this information is not important to the functioning of the events).

C#
Tuple<string, string> user = Tuple.Create<string, string>("user", "engineId");
securityLevelCache = new Hashtable();
securityLevelCache.Add(user, 2); // user has security level 2 (Authentication and Privacy)

Tuple<string, string> passwords = Tuple.Create<string, string>("authpass", "encryptpass");
passwordCache = new Hashtable();
passwordCache.Add(user, passwords);

Handling the GetUserSecurityLevel Event

Anytime an SNMPv3 secure message is received, the GetUserSecurityLevel event is fired to determine the correct security level for the User provided in the event parameter.

To handle this event, you must set the SecurityLevel parameter to one of the following:

  • -1: Ignore the message and fire the BadPacket event.
  • 0: No security. The message will be processed without any authentication and/or encryption.
  • 1: Authentication only. The message will be checked for a valid signature and the GetUserPassword event will be fired to verify the authentication password.
  • 2: Authentication and Privacy. The message will be checked for a valid signature and the GetUserPassword event will be fired twice; first to verify the authentication password, and then to verify the encryption password.

C#
// GetUserSecurityLevel event handler.
snmpagent1.OnGetUserSecurityLevel += (s, e) => {
    Tuple<string, string> user = Tuple.Create<string, string>(e.User, e.EngineId);
    if (securityLevelCache.Contains(user))
        e.SecurityLevel = (int)securityLevelCache[user]; // Would be set to 2 for "user".
    else
        e.SecurityLevel = -1; // The BadPacket event will be fired now.
};

Handling the GetUserPassword Event

After the GetUserSecurityLevel event is handled, the GetUserPassword event will fire 0-2 times (based on what the user's security level is) to verify the authentication password and, if applicable, encryption password. You'll need to handle this event slightly differently based on the value of the PasswordType parameter, which will be set to either 1 (authentication) or 2 (encryption).

For Authentication (1):
Set the Password parameter to the authentication password for the user specified by the User parameter, and set the Algorithm parameter to the user's authentication algorithm, which is one of the following:

  • 0 - Any (Special case, causes the component try all supported authentication algorithms until it finds the correct one)
  • 1 - HMAC-MD5-96
  • 2 - HMAC-SHA-96
  • 3 - HMAC-192-SHA-256

For Encryption (2):
Set the Password parameter to the encryption password for the user specified by the User parameter, and set the Algorithm parameter to the user's encryption algorithm, which is one of the following:

  • 1 - DES
  • 2 - AES
  • 3 - 3DES
  • 4 - AES192
  • 5 - AES256

C#
// GetUserPassword event handler.
snmpagent1.OnGetUserPassword += (s, e) => {
    e.Password = "";
    Tuple<string, string> user = Tuple.Create<string, string>(e.User, e.EngineId);
    if (passwordCache.Contains(user)) {
        switch (e.PasswordType) {
            case 1: // Asking for user's authentication password.
                e.Password = ((Tuple<string, string>)passwordCache[user]).Item1;
                e.Algorithm = 0;
                break;
            case 2: // Asking for user's privacy (encryption) password.
                e.Password = ((Tuple<string, string>)passwordCache[user]).Item2;
                e.Algorithm = 1;
                break;
        }
    }
};




Article Code Snippet Notes

It is assumed that all SNMPAgent code snippets in the article include the following code:

SNMPAgent Code (C#)
// List of SNMPObjects with example values.
List<SNMPObject> data = new List<SNMPObject> {
    new SNMPObject {Oid = "1.3.6.1.2.1.1.1", Value = "Description",
                    ObjectType = SNMPObjectTypes.otOctetString},
    new SNMPObject {Oid = "1.3.6.1.2.1.1.2", Value = "1.3.6.1.4.1.311", 
                    ObjectType = SNMPObjectTypes.otObjectId},
    new SNMPObject {Oid = "1.3.6.1.2.1.1.3", Value = snmpagent1.SysUpTime.ToString(), 
                    ObjectType = SNMPObjectTypes.otTimeTicks},
    new SNMPObject {Oid = "1.3.6.1.2.1.1.4", Value = "Contact Email: person@fake.com",
                    ObjectType = SNMPObjectTypes.otOctetString},
    new SNMPObject {Oid = "1.3.6.1.2.1.1.5", Value = "device1",
                    ObjectType = SNMPObjectTypes.otOctetString},
    new SNMPObject {Oid = "1.3.6.1.2.1.1.6", Value = "location",
                    ObjectType = SNMPObjectTypes.otOctetString},
    new SNMPObject {Oid = "1.3.6.1.2.1.1.7", Value = "72", 
                    ObjectType = SNMPObjectTypes.otInteger}
};

public SNMPObject get(string oid) {
    for (int i = 0; i < data.Count; i++)
        if (data[i].Oid.Equals(oid))
            return data[i];
    return null;
}

public SNMPObject getNext(string oid) {
    for (int i = 0; i < data.Count; i++)
        if (data[i].Oid.Equals(oid) && (i + 1) < data.Count)
            return data[i + 1];
    return null;
}

We appreciate your feedback.  If you have any questions, comments, or suggestions about this article please contact our support team at kb@nsoftware.com.