SNMP protocol, SNMP#Net implementation and API documentation.
Simple Network Management Protocol provides a way for devices to make state information available to interested parties and to send notifications of events.
SNMP defines two types of devices. Agents are devices that contain the state information and send notifications. Managers are devices that query state information and receive notifications.
SnmpSharpNet library focuses on the manager operation. To enable users to use the library, basic understanding of SNMP agent operation is required.
Over the years SNMP has been updated and has moved through version 1, 2c and 3. While it would not be unreasonable to expect that newer versions of a protocol supersede the older one, that is not the case with SNMP. You are more likely to run into an agent running protocol version 1 then version 3.
As a general rule you can reasonable expect that agents support SNMP version 1 and 2c. Version 3 is supported by ever expanding number of agents but it's still not used widely outside of organizations with strict risk requirements necessitating version 3.
Here is a high level comparison between SNMP protocol versions:
| Feature | Version 1 | Version 2c | Version 3 |
|---|---|---|---|
| GET request | Yes | Yes | Yes |
| GET-NEXT request | Yes | Yes | Yes |
| SET request | Yes | Yes | Yes |
| GET-BULK request | No | Yes | Yes |
| TRAP notification | Yes | Yes | Yes |
| INFORM notification | No | Yes | Yes |
| Community based security | Yes | Yes | No |
| User based security | No | No | Yes |
| Message Authentication | No | No | Yes |
| Message Encryption (privacy) | No | No | Yes |
Agents store state information in a tree like structure that is indexed using a list of integer values called object identifiers. Example object identifier is .1.3.6.1.2.1.1.1.0.
Object identifier individual values are identifiers or instances. For example, a single value is referred as Oid .1.3.6.1.2.1.1.1 with the instance 0 so the full "path" to the value is .1.3.6.1.2.1.1.1.0 and that is what you would query to get a sysDescr value from a device.
SNMP agents can also provide table information. To demonstrate we'll have a look at ifTable. Interface table contains information about device interfaces. ifTable has a base table Oid of 1.3.6.1.2.1.2.2, ifTable has entries under ifEntry which adds instance 1 to the table oid so the new ifTable.ifEntry oid is: 1.3.6.1.2.1.2.2.1. Each interface entry (ifEntry) contains individual interface values. For example, interface name is stored undef ifDescr object id which can be represented as ifTable.ifEntry.ifDescr or numerically as 1.3.6.1.2.1.2.2.1.2. Similarly, interface speed is ifTable.ifEntry.ifSpeed or 1.3.6.1.2.1.2.2.1.5
Now that you understand how individual table entries are accessed we need to talk how multiple rows within a table are accessed. As mentioned earlier individual values are accessed using unique identifiers. So in the interface table example above, we would have first interface referenced using instance value 1, second with instance value 2, etc. That means that first interface name will have object id: 1.3.6.1.2.1.2.2.1.2.1.
It is not a requirement that instance id be a single value. In some cases instance value can contain multiple id values. For this reason it is always best to check how many Id's are following the value object id you are retrieving.
So let's try to represent this in a more visual way:
Oid example
I am hoping that you can follow the above and that indexing of SNMP values on an agent makes sense. Just to clear up one thing that seams to confuse a lot of people. Object identifiers are numerical values. Names like ifTable, ifEntry, etc. are used when writing MIB files and are in no way part of the SNMP protocol itself. They are just a human readable "helper".
Now that you understand how information is organized on the agent, lets talk about what kind of information is stored by the agent.
Data values that object identifiers point to are of a few base types. Data types available in Snmp#Net library are:
| Data type | Description |
|---|---|
| Integer32 | signed 32-bit integer |
| Counter32 | same as Integer32, signed 32-bit integer (equivalent to Counter in SMIv1) |
| Gauge32 | unsigned 32-bit integer (equivalent to Gauge in SMIv1) |
| OctetString | byte array used to store both binary data and text strings |
| ObjectId | integer array representing an object identifier |
| Counter64 | signed 64-bit integer (only available in SMIv2) |
| TimeTicks | unsigned 32-bit integer representing time in 1/100 of seconds (10ms) |
| Null | Empty or no value |
Each Oid in the MIB is matched with a value. Value can be any of the above listed types (including another Oid or a Null value).
Oid and Value pair are known as VariableBinding. Each SNMP request and reply consists of one or more VariableBindings.
So a simple exchange between a manager and an agent would look like:
manager => GET Oid(1.3.6.1.2.1.1.1.0) agent => RESPONSE Oid(1.3.6.1.2.1.1.1.0) OctetString(my servers sysDescription)
Every version of Simple Network Management Protocol supports the following 3 request types:
SNMP versions 2c and 3 support an additional request type called GET-BULK.
GetBulk request performs multiple GetNext requests and returns the result in a single response. Besides the request Oid that is in every SNMP request type, GetBulk introduces two additional options. They are non-repeaters, tells the agent how many Oid's in the request should be treated as Get request variables, and max-repetitions, telling the agent how many GetNext operations to perform on each request variable (that is not covered by the non-repeaters option) and return the values in a single reply.
This is a little much to understand so here is an example you will probably need to use. Lets assume you wish to collect utilization information for all interfaces on an agent. One of the issues with utilization information is that you need a reliable time information so you can calculate the returned value delta between two retrieved utilization values. You cannot use the clock on the manager system because delay between replies can vary request by request (network delay can be 50ms one request and then 150ms on the next and then 2 second) making the delta value very unreliable.
To work around the reliable clock issue you can use sysUpTime value on the agent. This is the value representing the time since agent started in 1/100 of a second (10 milliseconds).
So now your request will need to retrieve sysUpTime and utilization information for all interfaces.
For this example we will assume that device has 10 interfaces with consecutive instance values.
This is what your request would look like:
Request-GetBulk non-repeaters = 1 max-repetitions = 10 sysUpTime.0 ifInOctets
Agent will respond to this request with:
Response: sysUpTime.0 : (TimeTicks)<some value> ifInOctets.1 : (Counter32)<some value> ifInOctets.2 : (Counter32)<some value> ifInOctets.3 : (Counter32)<some value> ifInOctets.4 : (Counter32)<some value> ifInOctets.5 : (Counter32)<some value> ifInOctets.6 : (Counter32)<some value> ifInOctets.7 : (Counter32)<some value> ifInOctets.8 : (Counter32)<some value> ifInOctets.9 : (Counter32)<some value> ifInOctets.10: (Counter32)<some value>
As you can see from the above, non-repeaters value has instructed the agent to treat the first Oid in the request (sysUpTime.0) as a Get request. Remaining Oid(s) in the request had GetNext operation performed max-repetitions times (in this case 10) and all values were returned in a single reponse.
GetBulk is considerably more efficient then other operations when multiple consecutive values need to be retrieved and you should use them whenever possible since, in the example above you have retrieved data that would require 1 x Get and 10 x GetNext requests to perform without GetBulk.
On a side note, if all you wish to perform are consecutive GetNext operation in a GetBulk request, set the non-repeaters value to 0.
Each request is contained in an appropriate (for the request) Protocol Data Unit.
Protocol Data Unit contains the type value, identifying operation type, ErrorStatus value, status of the operation (0 = noError, != 0 error), ErrorIndex, index of the value that is the cause of the error RequestId, unique identifier that helps to match requests and replies, and VariableBinding list that contains Oid to value mappings that are part of the request or reply.
Notifications are state or event updates sent by the agent to pre-configured receivers. There 2 types of notifications available in the SNMP standard, traps and informs.
Only difference between traps and informs is that traps and unacknowledged (kind of send and forget) messages sent by agents. Informs are expected to be acknowledged by receivers and sender agent will perform pre-configured number of retries before giving up.
SNMP Traps are supported in all versions of SNMP. Informs are supported in SNMP versions 2c and 3.
SNMP version 1 trap packet format is different from packets used in SNMP versions 2c and 3. When SNMP version 1 protocol standard was released notifications were viewed as needing a completely separate packet format from request/reply packets to perform their function. With SNMP version 2c traps start using standard request/reply packet format to send notifications.
Trap notification simulation
SNMP Trap example
To demonstrate how notifications work, lets use a simple example. On a router agent interface goes down. This is an event that requires a notification be sent. Agent constructs a notification packet (either trap or inform) and sends it to all pre-configured destinations. When packet is received by the manager application, if notification is an inform response is constructed and send back to the agent. If packet is trap notification is processed by the manager without any kind of acknowledgement sent back to the agent.
Inform notification simulation
SNMP Inform example
SNMP version 1 has a distinct packet format. Two values that SNMP version 1 trap packet contains are Generic Error code and Specific Error code. Generic Error is used to send a notification about a limited number of predefined events. Things like LinkUp, LinkDown are pre-defined.
One of the Generic Error codes is a value (EnterpriseSpecific) that specifies that notification refers to a vendor specific value. When Generic Event value is set to this, Specific Event specifies vendor specific error codes.
With the introduction of SNMP version 2c dedicated SNMP Trap packet that was used in version 1 of the protocol has been obsoleted. Standard SNMP request/reply packet is used with two values that have to be added to the beginning of the variable binding list.
SNMP version 2 trap variable binding list starts with the sysUpTime.0 value and trapObjectID.0 value. trapObjectID.0 value is an Oid value that tells the receiver what kind of event has occurred. Additional Oid/Value pairs follow the two mandated values further describing the event.
SNMP v2c and v3 traps are unacknowledged notifications. Agent has no idea if any of the notification receivers that it sent traps to have received them and it doesn't care.
Simple Network Management Protocol packet version 1 and version 2c use the same format because they both depend on community based security. Community based security depends on a shared community name value between the manager and agent for information authenticity to be verified.
SNMP packet is the first level sequence encoding that wraps all SNMP data sent in the packet. Packet sequence encoding is very simple:
Packet sequence contains following values encoded within it:
As you can see SNMP v1 and v2c packet format is very simple. Packet itself does not contain a lot of useful information. When processing incoming SNMP packet you only need to verify that you are dealing with the right protocol version and that community name matches the value you expected.
All useful information is stored in the Protocol Data Unit (Pdu class) which will be described in more detail in another article.
Here is a quick explanation of SNMP variable types as implemented in SnmpSharpNet.
First from the high level, all variable type classes are derived from the AsnType class. AsnType is an abstract class that forces derived classes to expose encode() and decode() methods and Type property.
Why is this important? encode() and decode() methods are used to encode values into a binary stream that can be transmitted to SNMP peers and decode() will decode those same binary encoded SNMP values into data you can work with in your .NET application.
Type property represents a one byte code identifying value type. As mentioned in introduction to SNMP, each value is encoded using TLV (Type, Length, Value) representation. This one byte value represents the "T" value of the TLV encoding.
Type property can be very handy when you are trying to evaluate Variable Binding value entries as they are stored as AsnType and to properly get access to the values or access methods specific to a SNMP value type you will need to cast the AsnType value to a specific class. For example, you can do the following:
AsnType val = new OctetString("This is my test string"); if (val.Type == SnmpConstants.SMI_STRING) { OctetString ostr = (OctetString)val; Console.WriteLine("Character 2 is {0}", ostr[1]); } else { Console.WriteLine("AsnType is not an OctetString class instance."); }
Have a look at the constants defined in SnmpConstants class starting with SMI_ for the available data types you can check this way.
Type property can be used to retrieve text name of the variable type specific class:
AsnType val = new OctetString("This is my test string"); Console.WriteLine("Variable [{0}] is type {1}", val.ToString(), SnmpConstants.GetTypeName(val.Type));
SNMP v1 and v2c Protocol Data Unit, represented by the Pdu class in the SnmpSharpNet library, contains request and reply values relevant to the data exchange manager initiated.
Protocol Data Unit is a sequence of values. Following values are encoded in the PDU sequence:
Two additional values are available for GetBulk PDUs:
Pdu type is set using PduType enumeration. Following values are defined in the enumeration:
PduType.Get; // Value: 0xa0 PduType.GetNext; // Value: 0xa1 PduType.Response; // Value: 0xa2 PduType.Set; // Value: 0xa3 PduType.GetBulk; // Value: 0xa5
Using the PduType enumeration you can set the Pdu type this way:
Pdu pdu = new Pdu(); pdu.Type = PduType.GetBulk;
There are convenience static methods available to help with the creation of different Pdu types:
Pdu getPdu = Pdu.GetPdu(vbs); Pdu getNextPdu = Pdu.GetNextPdu(vbs); Pdu getBulkPdu = Pdu.GetBulkPdu(vbs); Pdu setPdu = Pdu.setPdu(vbs);
I'll expain what the vbs argument in the above examples is later on...
Request id is a unique identifier used to match requests and responses. Pdu class by default sets the request id to a random value using the standard Random class that is part of the .NET framework.
If you wish to reset the request id to a new random value, just set the request id value to 0 (zero). This action will cause the encoding routine to retrieve a new random request id:
Pdu pdu = new Pdu(); pdu.RequestId = 0;
Above example will result in the Pdu being encoded with a new random value. This is mostly useful when you are reusing a Pdu for multiple requests. By setting the RequestId value to 0 you can ensure that each request is sent with a different request id.
Another way to reuse a Pdu class instance is to call the Pdu.Reset() method. When called, Reset method sets the RequestId value to the previous value incremented by 1. Request id value will rollover to 1 when maximum value of integer variable is reached to avoid overflow exceptions.
Error status is a value used to represent error that was encountered by the agent when processing the request. Error status values can be tested using PduErrorStatus enumeration. Error status value of PduErrorStatus.NoError (or value of 0 - zero) signifies that there was no error. When there is no error in the request, Error index value will be set to 0.
For different error status codes and their meaning, have a look at PduErrorStatus code documentation.
Error index is used when error reported in Error status is related to a specific variable in the Pdu. In all other cases (like if there is no error or if error is not related to a variable) Error index value is set to zero. If Error index value is set to value 1+ then that value points to a variable binding in the variable binding list.
When processing replies it is critically important to verify that ErrorStatus value does not contain an error. If error is returned and you continue to process the response there is a good chance further processing will fail.
Here is a full example of how to handle a response:
VbCollection vbCol = new VbCollection(); vbCol.Add("1.3.6.1.2.1.1.1.0"); vbCol.Add("1.3.6.1.2.1.1.2.0"); // vbCol.Add("1.3.6.1.2.1.1.1.1.0"); Pdu getPdu = Pdu.GetPdu(vbCol); UdpTarget target = new UdpTarget(IPAddress.Loopback); AgentParameters agentParams = new AgentParameters(SnmpVersion.Ver2, new OctetString("public")); SnmpV2Packet response = (SnmpV2Packet)target.Request(getPdu, agentParams); if (response != null) { if( response.Pdu.ErrorStatus == (int)PduErrorStatus.noError ) { Console.WriteLine("Response id {0}\n1: {1}\n2: {2}", response.Pdu.RequestId, response.Pdu.VbList[0].Value.ToString(), response.Pdu.VbList[1].Value.ToString()); Console.WriteLine("Error status: {0}", ((PduErrorStatus)response.Pdu.ErrorStatus).ToString()); } else { Console.WriteLine("Response id {0}", response.Pdu.RequestId); Console.WriteLine("Error code: {0} index: {1} name: {2}", response.Pdu.ErrorStatus, response.Pdu.ErrorIndex, ((PduErrorStatus)response.Pdu.ErrorStatus).ToString()); } } target.Dispose();
Important part to focus on is the processing of response.Pdu.ErrorStatus. Before focusing attempting to process data in the packet you will want to check if ErrorStatus value is equal to noError. Any other value represents an error and data in the packet is suspect.
Pdu class provides helper methods to make access to the variable binding collection easier. You can access VarBind collection directly through an indexer in the Pdu class itself (without using Pdu.VbList collection) or by accessing Pdu class enumerator:
// SnmpV2Packet response = (SnmpV2Packet)target.Request(pdu, param); foreach (Vb v in response.Pdu) { Console.WriteLine("{0}: {1}", v.Oid.ToString(), v.Value.ToString()); } // or for (int i = 0; i < response.Pdu.VbCount; i++) { Console.WriteLine("{0} - {1}: {2}", i, response.Pdu[i].Oid.ToString(), response.Pdu[i].Value.ToString()); }
VarBind collection access helpers were introduced in library version 0.7.8
Variable Bindings represent a set of Oid/Value pairs.
As described in other pages, each value available through SNMP is identified by a unique object identifier. To enable mapping of object identifiers to their values a special sequence is required. That sequence is variable binding.
Most requests involve multiple OIDs so a protocol data unit needs to have a way to encode multiple variable bindings. This is accomplished by encoding all variable bindings into an outer sequence TLV (where the value is multiple variable bindings).
Here is a graphical representation of a variable binding collection:
Individual Variable Bindings are stored in the Vb class. Multiple Variable Bindings are controlled by the Variable Binding collection represented by the VbCollection class.
Standard request (Get, GetNext or GetBulk) contains one or more Variable Bindings with the Object Identifier of the requested value and value of Null (as in SNMP Null not c# null). Agent will replace Null value with the actual value of the object identifier identified value in the reply.
One way to create Variable Binding(s) for a request is to construct individual variable binding entries and to add them to the Variable Binding collection:
Create a variable binding and add the Object identifier in string format:
Vb vb = new Vb("1.3.6.1.2.1.1.1.0");
Create a variable binding and add the Object identifier in Oid format:
Oid oid = new Oid("1.3.6.1.2.1.1.1.0"); Vb vb = new Vb(oid);
Variable Binding on its own is not very useful. SNMP requests, replies and notifications are collections of zero or more Variable Bindings. To make use of Variable Binding that you created it has to be added to a Variable Binding collection:
VbCollection vbcol = new VbCollection(); vbcol.Add(vb);
or VarBind collection that is part of a Pdu class:
Pdu pdu = new Pdu(); pdu.VbList.Add(vb);
Variable Bing Vb class provides two data access properites: Vb.Oid of type Oid and Vb.Value of type AsnType.
Oid property is the Oid assigned to the Variable Binding and using this property you can access all methods and properties available in the Oid class.
Value property is returned SMI value cast to AsnType. AsnType is an abstract class that all SNMP value types implemented in Snmp#Net library are derived from. One of the properties AsnType class has is AsnType.Type which allows you to query the type of data and related Snmp#Net class that is represented by the Value property. Type code constants are defined as part of the SnmpConstants class and begin with SMI_ (for example Counter32 is defined as SnmpConstants.SMI_COUNTER32). Example:
Vb v = new Vb(new Oid("1.3.6.1.2.1.1.7.0"), new Integer32(78)); if (v.Value.Type == SnmpConstants.SMI_INTEGER) { Integer32 val = (Integer32)v.Value; Console.WriteLine("Integer value: {0}", val.Value); } else { Console.WriteLine("Unknown data type: {0}", v.Value.Type); } // Prints: Integer value: 78
Variable Binding collection is managed using Snmp#Net VbCollection class.
Internally VbCollection class manages a list of Vb class instances, each with its own Oid/Value pair. During encoding of the Pdu VbCollection belongs to, individual variable bindings are encoded in the order they were added.
Methods available in the VbCollection class used to manage individual variable binds are:
VbCollection.Add(...) methods add new Vb instances to the end of the list of managed variable bindings. Available formats are:
VbCollection vbCol = new VbCollection(); Oid oidinst = new Oid("1.3.6.1.2.1.1.1.0"); // Add a Vb with Oid and value Null (value is auto-added) vbCol.Add(oidinst); OctetString vbVal = new OctetString("New string value"); // Add a Vb with Oid and specific Value vbCol.Add(oidinst, vbVal); Vb vb = new Vb(oidinst, vbVal); // Add pre-initialized Vb class instance vbCol.Add(vb); VbCollection vbCol2 = new VbCollection(); vbCol2.Add(new Oid("1.3.6.1.2.1.1.2.0")); // Add contents of another VbCollection vbCol.Add(vbCol2);
If you would like to change ordering of Vb instances within the VbCollection, you can use VbCollection.Insert(...) method:
Vb vb = new Vb(new Oid("1.3.6.1.2.1.1.1.0"), new OctetString("New string value")); // Add new Vb instance to the beginning of the Vb collection vbCol.Insert(0, vb);
You can remove Vb instances from the collection using VbCollection.RemoveAt(...) method:
// Loop through all Vb's in collection until collection is emptied while( vbCol.Count > 0 ) { Vb vb = vbCol[0]; Console.WriteLine(vb.ToString()); vbCol.RemoveAt(0); }
You can access Vb's stored in the VbCollection using multiple methods. First is using indexed access:
for(int i=0;i<vbCol.Count;i++) { Console.WriteLine(vbCol[i].ToString()); }
You can access individual Vb's using their Oid value (if you know it):
Oid vbOid = new Oid("1.3.6.1.2.1.1.1.0"); Pdu pdu = new Pdu(PduType.Get); pdu.VbList.Add(vbOid); // perform get operation, check that there were no errors, etc. // assuming response Pdu is stored in the pdu variable if( pdu.VbList.ContainsOid(vbOid) ) { Console.WriteLine("Value: {0}", pdu.VbList[vbOid].Value.ToString()); }
You can also use string representation of the Vb Oid:
String oidString = "1.3.6.1.2.1.1.1.0"; Pdu pdu = new Pdu(PduType.Get); pdu.VbList.Add(oidString); // perform get operation, check that there were no errors, etc. // assuming response Pdu is stored in the pdu variable Vb vbResult = pdu.VbList[oidString]; if( vbResult != null ) { Console.WriteLine("Value: {0}", pdu.VbList[oidString].Value.ToString()); }
There is also IEnumerator interface that is implemented:
foreach( Vb vb in pdu.VbList ) { Console.WriteLine("{0}: {1}", vb.Oid.ToString(), vb.Value.ToString()); }
Or you can retrieve the list of Vb Oid values and then process them one at the time:
Oid[] oidList = pdu.VbList.OidArray(); foreach( Oid o in oidList ) { Vb v = pdu.VbList[o]; if( v != null ) Console.WriteLine("{0}: {1}", o.ToString(), v.Value.ToString());
Retrieving the list of Vb Oid's from a VarBind collection is an inefficient process because each Oid instance stored in the class is duplicated to maintain class values integrity. Because of the duplication additional memory and CPU time is used that is not strictly speaking necessary. Other access methods described here do not have this down side.
Finally, when you are done with the VbCollection and would like to re-use it for additional requests, call the VbCollection.Clear() method to remove all the Vb's and prepare it for a new operation.
Check out Snmp#Net documentation site for the list of other methods.
IP address is the standard IP version 4 address used to uniquely identify individual hosts connected to the Internet (or private networks). In the Snmp#Net library, IP address operations are performed using IpAddress class.
IpAddress class is used both as part of SNMP request/reply communication and the supporting framework that handles packet operations. As such, it contains a lot of functionality that is not strictly needed by the SNMP but can be handy when writing real world applications.
IpAddress class is derived from the OctetString class and contains exactly 4 bytes of data (representing 4 octets of the IP address value). Internally, IP address value is stored in bit-endian order as is required for encoding and transmission between hosts. Where value is returned as numeric UInt32 value (4 byte unsigned integer value representing the IP address), byte array is converted into a little endian ordered numeric value suitable for mathematical operations.
First, what is an IP address? As mentioned earlier, IP address is a 4 byte value usually represented in dotted decimal format. For example, 192.168.20.11 is a valid IP address.
Now that IP address format is explained, creating an IpAddress class instance can be done with a default IP address of 0.0.0.0 by calling a constructor without arguments:
IpAddress ipaddr = new IpAddress(); Console.WriteLine("IpAddress: {0}", ipaddr.ToString()); // Prints: IpAddress: 0.0.0.0
It is usually easier if you initialize the class during initialization. One of the ways to do that is to pass the 4 byte array to the constructor:
IpAddress ipaddr = new IpAddress(new byte[] { 192, 168, 20, 11 }); Console.WriteLine("IpAddress: {0}", ipaddr.ToString()); // Prints: IpAddress: 192.168.20.11
Another way to initialize the class is to pass the initial IP address value as a dotted decimal formatted string:
IpAddress ipaddr = new IpAddress("192.168.20.11"); Console.WriteLine("IpAddress: {0}", ipaddr.ToString()); // Prints: IpAddress: 192.168.20.11
Problem with dotted decimal IP address representation is that nobody (or nobody you want to know) can remember IP addresses of all the hosts they need to work with. For that reason, a smart egg somewhere out there in the Internet land came up with the Domain Name Service (DNS) concept. DNS maps difficult to remember IP addresses to somewhat easier to remember host names.
Host name can be resolved to an IP address using DNS name resolution. IpAddress constructor that accepts a dotted decimal string representation of an IP address will also attempt domain name resolution if parsing of dotted decimal format fails. Be careful with this feature. Domain name resolution is performed using blocking DNS calls that can result in thread blocking for a while (if DNS query response is slow) and make your application unresponsive. It is a good idea to resolve IP addresses prior to passing them to the IpAddress class to improve application responsiveness.
Here is DNS resolution in the IpAddress constructor example:
IpAddress ipaddr = new IpAddress("yahoo.com"); Console.WriteLine("IpAddress: {0}", ipaddr.ToString()); // Prints: IpAddress: 69.147.125.65
Be careful with the name resolution in the constructor. If the name you specified in the argument fails to resolve, constructor will throw an exception:
IpAddress ipaddr; try { ipaddr = new IpAddress ("nonexistenthost.com"); } catch (Exception ex) { Console.WriteLine ("Exception {0}: {1}", ex.GetType ().ToString (), ex.Message); ipaddr = null; } if( ipaddr != null ) Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: // Exception System.ArgumentException: Unable to parse or resolve supplied value to an IP address. // Parameter name: value
Another way to initialize the IpAddress class is to pass an instance of the System.Net.IPAddress:
IPAddress iptmp = IPAddress.Parse ("192.168.20.11"); IpAddress ipaddr = new IpAddress (iptmp); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: IpAddress: 192.168.20.11
Being able to initialize IpAddress class from System.Net.IPAddress can be handy if you are initializing it with a value returned by Socket.ReceiveFrom method for example.
Since IP address is a 4 byte value, it is possible to represent it as an unsigned 32-bit integer. If you can represent the value with it then you should be able to initialize the class with it:
IpAddress ipaddr = new IpAddress (185903296); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: IpAddress: 192.168.20.11
Now that IpAddress class is constructed, you can check validity of the value it represents by calling IpAddress.Valid property. Property value will be true if internal value is 4 bytes in length and at least one of the 4 values is not 0 (zero). While all zero IP address (0.0.0.0) is valid it doesn't have a useful purpose so it is considered invalid for the validity purpose.
You can change the value pre-initialized IpAddress class represents by using IpAddress.Set(..) methods. There is a set method matching the syntax of every constructor described above:
IpAddress ipaddr = new IpAddress (); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: IpAddress: 0.0.0.0 ipaddr.Set (new byte[] { 192, 168, 20, 11 }); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: IpAddress: 192.168.20.11 ipaddr.Set ("192.168.20.12"); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: IpAddress: 192.168.20.12 ipaddr.Set ("yahoo.com"); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: IpAddress: 69.147.125.65 /********************* * On the TODO list: * ********************* IPAddress iptmp = IPAddress.Parse ("192.168.20.11"); ipaddr.Set(iptmp); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); */ ipaddr.Set(185903282); Console.WriteLine ("IpAddress: {0}", ipaddr.ToString ()); // Prints: IpAddress: 178.168.20.11
As you can see from the above examples, IpAddress value can be retrieved as a string in dotted decimal format using IpAddress.ToString() method.
Additionally, you can get the IpAddress value as a UInt32 using IpAddress.GetUInt32() method or as a byte array, using IpAddress.ToArray() method inherited from OctetString class.
IpAddress ipaddr = new IpAddress (new byte[] { 192, 168, 20, 11 }); Console.WriteLine ("IpAddress UInt32: {0} ({1})", ipaddr.ToUInt32 (), ipaddr.ToString ()); // Prints: IpAddress UInt32: 185903296 (192.168.20.11) byte[] ipbuf = ipaddr.ToArray (); SnmpConstants.DumpHex (ipbuf); // Prints: 0000 c0 a8 14 0b
Value returned by IpAddress.ToUInt32() method is not suitable for performing increment operations on the IP address because it is formatted in the big-endian byte order. See helper methods section for increment, decrement, etc. operation methods.
Access to individual byte values comprising the IP address can be made using indexed accessor:
IpAddress ipaddr = new IpAddress (new byte[] { 192, 168, 20, 11 }); for (int i = 0; i < ipaddr.Length; i++) { Console.Write ("{0}:{1} ", i, ipaddr[i]); } Console.WriteLine (""); // Prints: 0:192 1:168 2:20 3:11
You can compare two IpAddress value using IpAddress.CompareTo() method:
/* Example comparison with second value greater then first */ IpAddress ipaddr1 = new IpAddress (new byte[] { 192, 168, 20, 11 }); IpAddress ipaddr2 = new IpAddress (new byte[] { 192, 168, 20, 12 }); int compResult = ipaddr1.CompareTo (ipaddr2); if (compResult == -1) Console.WriteLine ("IpAddress {0} is less then {1}", ipaddr1.ToString (), ipaddr2.ToString ()); else if (compResult == 0) Console.WriteLine ("IpAddress {0} is equal to {1}", ipaddr1.ToString (), ipaddr2.ToString ()); else if (compResult == 1) Console.WriteLine ("IpAddress {0} is greater then {1}", ipaddr1.ToString (), ipaddr2.ToString ()); // Prints: IpAddress 192.168.20.11 is less then 192.168.20.12 /* Example comparison with first value greater then second */ ipaddr1.Set (new byte[] { 192, 168, 20, 12 }); ipaddr2.Set (new byte[] { 192, 168, 20, 11 }); compResult = ipaddr1.CompareTo (ipaddr2); if (compResult == -1) Console.WriteLine ("IpAddress {0} is less then {1}", ipaddr1.ToString (), ipaddr2.ToString ()); else if (compResult == 0) Console.WriteLine ("IpAddress {0} is equal to {1}", ipaddr1.ToString (), ipaddr2.ToString ()); else if (compResult == 1) Console.WriteLine ("IpAddress {0} is greater then {1}", ipaddr1.ToString (), ipaddr2.ToString ()); // Prints: IpAddress 192.168.20.12 is greater then 192.168.20.11 /* Example comparison of equal values */ ipaddr2.Set (new byte[] { 192, 168, 20, 12 }); compResult = ipaddr1.CompareTo (ipaddr2); if (compResult == -1) Console.WriteLine ("IpAddress {0} is less then {1}", ipaddr1.ToString (), ipaddr2.ToString ()); else if (compResult == 0) Console.WriteLine ("IpAddress {0} is equal to {1}", ipaddr1.ToString (), ipaddr2.ToString ()); else if (compResult == 1) Console.WriteLine ("IpAddress {0} is greater then {1}", ipaddr1.ToString (), ipaddr2.ToString ()); // Prints: IpAddress 192.168.20.12 is equal to 192.168.20.12
You can do the same comparison of IpAddress class value against an instance of System.Net.IPAddress class. Results will be the same as in the above example.
IpAddress.Equals() method only compares the class value against another IpAddress class. This will be expanded in the next revision of the library (current version is 0.7.8) to include capability to compare with IPAddress, byte array, and UInt32 values.
I frequently have to increment IP addresses in the process of scanning subnetworks for hosts of interest. It seamed handy to be able to do that directly in the IpAddress class. It contains the right value, it can perform basic DNS resolution, and I'm using it anyway for my SNMP requests.
As discussed earlier, IP address is a 4 byte value in big-endian byte order that can be represented as an unsigned integer. It was also mentioned that this value is not suitable for incrementing and decrementing IP address value because it is in big-endian byte order. What this means in real terms is this:
IpAddress ipaddr = new IpAddress("192.168.11.23"); Console.WriteLine("IpAddress: {0}", ipaddr.ToString()); // Prints: IpAddress: 192.168.11.23 UInt32 ipNum = ipaddr.ToUInt32(); ipNum += 30; ipaddr.Set(ipNum); Console.WriteLine("Incremented IpAddress: {0}", ipaddr.ToString()); // Prints: Incremented IpAddress: 222.168.11.23
As you can see from the example, wrong part of the IP address is incremented and that is because byte order of the integer is wrong. We can fix that with the static helper method IpAddress.ReverseByteOrder(UInt32) method:
IpAddress ipaddr = new IpAddress("192.168.11.21"); Console.WriteLine("IpAddress: {0}", ipaddr.ToString()); // Prints: IpAddress: 192.168.11.21 UInt32 ipNum = ipaddr.ToUInt32(); UInt32 revIpNum = IpAddress.ReverseByteOrder(ipNum); revIpNum += 25; ipNum = IpAddress.ReverseByteOrder(revIpNum); ipaddr.Set(ipNum); Console.WriteLine("Incremented IpAddress: {0}", ipaddr.ToString()); // Prints: Incremented IpAddress: 192.168.11.46
That's better. Reversing the byte order within the unsigned integer has converted the IP address value into a representative value that you can perform any mathematical operation on, reverse the byte order again and apply to the IpAddress class and presto, you've got the IP address you where looking for.
While above example is handy to demonstrate how to get the IpAddress value that you can mathematically process, it is a lot of work to just increment an IP address. It's considerably easier to just use IpAddress.Increment(UInt32) method:
IpAddress ipaddr = new IpAddress("192.168.11.21"); Console.WriteLine("IpAddress: {0}", ipaddr); // Prints: IpAddress: 192.168.11.21 IpAddress procAddr = ipaddr.Increment(1); Console.WriteLine("IpAddress + 1: {0}", procAddr); // Prints: IpAddress + 1: 192.168.11.22 procAddr = ipaddr.Increment(5); Console.WriteLine("IpAddress + 5: {0}", procAddr); // Prints: IpAddress + 5: 192.168.11.26
With the basic math on IpAddress values is possible, you need something to do the math on. For that you will need to be able to get subnet information so you can get your range of addresses to scan.
Let's look at a real example. If you access a router and retrieve the list of IP addresses in the ipAddrTable (RFC1213-MIB). The value you get are ipAdEntAddr holding the IP address and ipAdEntNetMask holding the subnet mask for the interface. Using these two values you can calculate the subnetwork address and begin a walk operation:
IpAddress ipaddr = new IpAddress("192.168.11.21"); Console.WriteLine("IpAddress: {0}", ipaddr); IpAddress submask = new IpAddress("255.255.255.192"); Console.WriteLine("SubnetMask: {0}", submask); /* Verify subnet mask is a valid value: */ if (submask.IsValidMask()) { IpAddress subAddr = ipaddr.GetSubnetAddress(submask); Console.WriteLine("Subnet address: {0}", subAddr); IpAddress bcastAddr = ipaddr.GetBroadcastAddress(submask); Console.WriteLine("Broadcast address: {0}", bcastAddr); Console.WriteLine("************"); IpAddress incrIP = (IpAddress)subAddr.Clone(); while (incrIP != bcastAddr) { if (incrIP == subAddr) Console.WriteLine("Subnet address: {0}", incrIP); else Console.WriteLine("Host address: {0}", incrIP); incrIP = incrIP.Increment(1); if( incrIP == bcastAddr ) Console.WriteLine("Broadcast address: {0}", incrIP); } } else { Console.WriteLine("SubnetMask {0} is not valid."); }
With the following result:
IpAddress: 192.168.11.21 SubnetMask: 255.255.255.192 Subnet address: 192.168.11.0 Broadcast address: 192.168.11.63 ************ Subnet address: 192.168.11.0 Host address: 192.168.11.1 Host address: 192.168.11.2 Host address: 192.168.11.3 Host address: 192.168.11.4 [...] Host address: 192.168.11.60 Host address: 192.168.11.61 Host address: 192.168.11.62 Broadcast address: 192.168.11.63
In the course of developing net management applications I have often had to build code to verify string content for a valid IP address. To help with that IpAddress class includes static method IpAddress.IsIP(string). This method will verify that string contains 3 full stops (.) separating 4 numbers with values in the range 0 to 255.
See class documentation for all methods available in the IpAddress class.
Object Identifier is a list of numbers representing path through the MIB tree to an eventual sub-tree or value. It is represented, in human readable form, as a list of dot separated decimal numbers. For example, object identifier 1.3.6.1.2.1.1.1.0 represents sysDescr.0 MIB variable.
Before we look into the workings of object ids I need to mention one thing. Object id should never have a value of NULL (or empty value). According to the standards, NULL or empty object id value is represented as (decimal) 0.0. A lot of the implementations do not follow this rule and still allow empty object id values to be encoded. Snmp#Net library can handle object ids encoded as null (0 length) values but it will always encode null object ids as standard compliant 0.0 values.
In Snmp#Net object identifiers are manipulated using Oid class.
Oid class provides you with the methods to create, manipulate, access, encode and decode object identifiers.
Lets start with how you create an Oid class.
You can create an empty Oid class (NULL class as mentioned above) by using the following constructor:
Oid myoid = new Oid();
This constructor will create an empty Oid class. You can test it this way:
if( myoid.IsNull ) Console.WriteLine("This class is null."); else Console.WriteLine("This class is NOT null.");
This is not very helpful, at least not to start with. We will get to describing actions you can take with the class once constructed that will make it useful later.
Let's see a way to create a class that would make it useful right from the beginning.
In this example, we will create an Oid class initialized with the sysDescr.0 object id in the constructor. As mentioned before, object identifier is a list of integers. As such, it makes sense to have a constructor that can take an array of Int32 values and initialize the newly created Oid class.
Here is how to do it:
UInt32[] oidval = new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1, 0 }; Oid myoid = new Oid(oidval); if (myoid.IsNull) Console.WriteLine("OID is null."); else { Console.WriteLine("OID length is: {0}", myoid.Length); Console.WriteLine("OID: {0}", myoid.ToString()); }
Output of the above will look something like this:
OID length is: 9 OID: 1.3.6.1.2.1.1.1.0
Now that the class has content you can add it to a request or compare it with another Oid class. Before going there, let's have a look at how you can clone an existing Oid class.
Getting a duplicate of the existing class, meaning another distinct Oid class representing the same object identifier without any references to the original Oid class, is simple.
You can clone the existing class:
Oid myoid2 = (Oid)myoid.Clone();
or you can construct another Oid class using the copy constructor:
Oid myoid2 = new Oid(myoid);
Trick here is that in both cases, object identifier values are duplicated and not copied by reference. This way you will create a new class with its own data buffer without any reference to the class that was the source of information.
Same is true when you create the class with a numeric array passed to the constructor (as in the earlier example). Snmp#Net never uses references of the data passed as arguments to methods or constructors, except where clearly stated.
There is one final way you can create an Oid class. The most common way object identifiers are presented to users is in dotted decimal notation. Since this is the most common way to present it, there is also a way to pass the object identifier to the Oid class constructor as a string representing the class.
Here is the final constructor example:
Oid myoid = new Oid("1.3.6.1.2.1.1.1.0"); Console.WriteLine("OID: {0}", myoid.ToString()); # Returns: OID: 1.3.6.1.2.1.1.1.0
This is the most inefficient way to construct an Oid class. Parsing of text and error testing takes much longer (longer is relative considering the speed of computers) then passing an array of integers to a constructor.
Every method of initializing the Oid class using the constructor is also possible with a pre-initialized class. You can use Oid.Set methods to set the class object identifier in the same way as with constructors:
UInt32[] oidval = new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1, 0 }; Oid myoid = new Oid(); myoid.Set(oidval); // or myoid.Set("1.3.6.1.2.1.1.1.0");
Each of the Oid.Set() operations deletes existing object identifier value stored in the class (if any) and sets the class value to the new object identifier.
One thing to keep in mind is that minimum length of an object identifier is 2 values. Single value object identifiers are encoded as null value (0.0).
You can access object identifier individual values as you would access values in any array:
UInt32[] oidval = new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1, 0 }; Oid myoid = new Oid(oidval); for (int i = 0; i < myoid.Length; i++) Console.Write("{0} ", myoid[i]); Console.WriteLine("");
To manipulate values, you can retrieve Oid class value as an array of integers using:
int[] oidval = myoid.ToArray();
As usual, Oid.ToArray() will create a duplicate of the internal class data and return that. Changing the returned array will not affect the original class contents.
There is a shortcut to this method, just cast an Oid class as int[]:
uint[] oidval = (uint[])myoid;
This does the same thing as Oid.ToArray() method.
Usually what you have to deal with when working with object identifier values is to add values to it. For example:
UInt32[] oidval = new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1 }; Oid myoid = new Oid(oidval); Console.WriteLine("OID: {0}", myoid.ToString()); // OID: 1.3.6.1.2.1.1.1 myoid.Add(0); Console.WriteLine("OID: {0}", myoid.ToString()); // OID: 1.3.6.1.2.1.1.1.0
You can add more then one value by supplying an array of integers to the Oid.Add() method or you can add another Oid class value or a string value.
Adding values to an Oid class can also be done using simple arithmetic:
UInt32[] oidval = new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1 }; Oid myoid = new Oid(oidval); Console.WriteLine("OID: {0}", myoid.ToString()); // OID: 1.3.6.1.2.1.1.1 myoid += 0; Console.WriteLine("OID: {0}", myoid.ToString()); // OID: 1.3.6.1.2.1.1.1.0
When working with object identifiers I tend to spend a lot of time comparing them. Oid class provides some helpful ways to do that.
First are Oid.Compare() methods. These methods will compare object identifiers (represented as another Oid class, an array of integers or a string value) up to the length of the shorter value. This means that if you are comparing 1.3.6.1 and 1.3.6.1.2.1.1, result will be 0 (values are the same). This behavior was created for compatibility with libraries I used a long time ago in c/c++ world.
If you wish to perform an exact comparison, then you should use Oid.CompareExact() methods. These methods will compare class values, including their length.
If you are looking for a quick and easy comparison, the Oid.Equals() and override operators == and != all use Oid.CompareExact() to check object identifier equality and are easy enough to use:
UInt32[] oidval = new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1 }; Oid myoid = new Oid(oidval); Oid myoid2 = new Oid(oidval); if (myoid == myoid2) Console.WriteLine("Equal: {0} == {1}", myoid.ToString(), myoid.ToString()); // Prints: Equal: 1.3.6.1.2.1.1.1 == 1.3.6.1.2.1.1.1 myoid2 += 0; if (myoid != myoid2) Console.WriteLine("NotEqual: {0} != {1}", myoid.ToString(), myoid2.ToString()); // Prints: NotEqual: 1.3.6.1.2.1.1.1 != 1.3.6.1.2.1.1.1.0 int compRes = myoid.Compare(myoid2); Console.WriteLine("Compare(\"{0}\" , \"{1}\") = {2}", myoid.ToString(), myoid2.ToString(), compRes); // Prints: Compare("1.3.6.1.2.1.1.1" , "1.3.6.1.2.1.1.1.0") = 0
Further comparison is possible with greater-then ">" and less-then "<" operators.
Oid class method that has to be described because it is probably the most used is the Oid.IsRootOf() method. When performing SNMP walk operation (multiple Get-Next or Get-Bulk) methods to retrieve an entire MIB sub-tree, you will need to make sure that object identifiers contained in the reply of your request are sub-identifiers of the original request. This is where this method shines.
Oid myoid1 = new Oid(new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1 }); Oid myoid2 = new Oid(new UInt32[] { 1, 3, 6, 1, 2, 1, 1, 1, 0 }); if (myoid1.IsRootOf(myoid2)) Console.WriteLine("Oid {0} is root of {1}", myoid1.ToString(), myoid2.ToString()); // Prints: Oid 1.3.6.1.2.1.1.1 is root of 1.3.6.1.2.1.1.1.0 myoid2.Set(new Int32[] { 1, 3, 6, 1, 2, 1, 1, 2, 0 }); if(! myoid1.IsRootOf(myoid2)) Console.WriteLine("Oid {0} is NOT root of {1}", myoid1.ToString(), myoid2.ToString()); // Prints: Oid 1.3.6.1.2.1.1.1 is NOT root of 1.3.6.1.2.1.1.2.0
In this example you can see both positive and negative root object identifier check.
Final method to review here is the Oid.GetChildIdentifiers(Oid, Oid) static method. This method can be very handy when you have retrieved a value with a specific Oid and wish to use the instance of that value to retrieve additional values.
Child identifiers are identifiers that are child values not present in the root value (first Oid argument).
Child identifiers are returned as an array of unsigned integers. If first argument is not a root of the second or if they are the same, this method will return null as there are no child identifiers in that scenario.
During normal use of the Snmp#Net library you do not have to worry about encoding and decoding individual variables. This process is taken care of by the Pdu and SNMP packet classes described elsewhere. For this reason I have excluded those methods from this description of the class. If you are interested in how encode/decode is done, look at the API documentation and library source code.
Octet String is an SMI data type used to process arrays of Byte values. Unlike what the name suggests, this data type is not limited to string values but can store any byte based data type (including binary data).
In the Snmp#Net library, Octet String data type is represented with the OctetString class.
Calling base OctetString constructor initializes a new instance of the class with the internal byte buffer set to null:
OctetString ostr = new OctetString(); Console.WriteLine("OctetString.Length: {0}", ostr.Length); // Prints: OctetString.Length: 0
If you have data you wish to initialize the OctetString class with, then there are class constructors that will allow you to do that at the class initialization:
Most basic constructor that initializes the class data takes a byte array as an argument:
byte[] buf = new byte[] { 0x11, 0xfa, 0xbb, 0xba, 0x00 }; OctetString ostr = new OctetString(buf); Console.WriteLine("OctetString.Length: {0}", ostr.Length); // Prints: OctetString.Length: 5 Console.WriteLine("OctetString: {0}", ostr.ToString()); // Prints: OctetString: 11 FA BB BA 00
In the above example, we have initialized the class with binary values (outside of the ASCII character set value range), and you can see that OctetString.ToString() method recognized that values contained within the class are not printable ASCII characters and it returned them as hex value representations formatted into a string.
You can test OctetString class instances to see if they contain non printable characters by accessing OctetString.IsHex property. This property has one caveat and that is that hex value 0x00 at the end of the buffer is not considered a hex value. This is a standard way of terminating strings in C/C++ programming languages and some of the agents encode OctetString data without trimming this character from the end of ASCII string values.
You can also force printable string to be returned as a hex value represented string by calling OctetString.ToHexString() method.
There is no way to force hex string to be represented as a printable ASCII string.
Coming back to the constructor example above, one feature of this example is that byte array that is passed to the class is duplicated before being assigned to the class internal value removing any relationship between the class instance and the argument.
This is easily demonstrated this way:
byte[] buf = new byte[] { 0x11, 0xfa, 0xbb, 0xba, 0x00 }; OctetString ostr = new OctetString(buf); Console.WriteLine("OctetString: {0}", ostr.ToString()); // Prints: OctetString: 11 FA BB BA 00 buf[0] = 0x10; Console.WriteLine("OctetString: {0}", ostr.ToString()); // Prints: OctetString: 11 FA BB BA 00
As you can see from the above example, once OctetString is initialized with the byte array argument, changes to the array do not affect contents of the class. This behavior comes with a slight performance and memory usage penalty as the new byte array is allocated for the internal use by the OctetString class and data is copied from the argument into the internal buffer.
If you wish to avoid this overhead, or would like to retain direct access to the byte array, you can assign data to the class "by reference" this way:
byte[] buf = new byte[] { 0x11, 0xfa, 0xbb, 0xba, 0x00 }; OctetString ostr = new OctetString(buf, true); Console.WriteLine("OctetString: {0}", ostr.ToString()); // Prints: OctetString: 11 FA BB BA 00 buf[0] = 0x10; Console.WriteLine("OctetString: {0}", ostr.ToString()); // Prints: OctetString: 10 FA BB BA 00
Second argument to the constructor of OctetString in this example is a boolean flag telling the class if internal class buffer should be set by reference (true) or not (false). When set to true, any changes to the byte array outside of the OctetString class will be reflected in the class value as demonstrated by the example above.
Since OctetString data type is used to transport both binary and ASCII text data, it only makes sense for there to be a constructor allowing class value to be set to a string value:
OctetString ostr = new OctetString("Example OctetString"); Console.WriteLine("OctetString length {0} isHex {1}: {2}", ostr.Length, ostr.IsHex.ToString(), ostr.ToString()); // Prints: OctetString length 19 isHex False: Example OctetString
Constructor that takes a string value as an argument only functions with UTF8 encoded string (single byte per character). For other encoding types, first convert the string to a byte array using System.Text namespace classes and then initialize the OctetString class with the result.
There are two more constructors available in the OctetString class. First is the copy constructor that takes another instance of OctetString as an argument and copies data from it into the new class instance. Second takes a single byte argument and initializes the class to length 1 and value of a single byte.
With a constructed OctetString you can manipulate data in a number of ways. First you can change class data by using OctetString.Set(...) methods. They mirror the constructors in the functionality the provide:
OctetString ostr = new OctetString(); Console.WriteLine("OctetString.Length: {0}", ostr.Length); // Prints: OctetString.Length: 0 byte[] buf = new byte[] { 0x11, 0xfa, 0xbb, 0xba, 0x00 }; ostr.Set(buf); Console.WriteLine("OctetString length {0}: {1}", ostr.Length, ostr.ToString()); // Prints: OctetString length 5: 11 FA BB BA 00 ostr.SetRef(buf); Console.WriteLine("OctetString length {0}: {1}", ostr.Length, ostr.ToString()); // Prints: OctetString length 5: 11 FA BB BA 00 buf[0] = 0x10; Console.WriteLine("OctetString length {0}: {1}", ostr.Length, ostr.ToString()); // Prints: OctetString length 5: 10 FA BB BA 00 ostr.Set("Example OctetString"); Console.WriteLine("OctetString length {0} isHex {1}: {2}", ostr.Length, ostr.IsHex.ToString(), ostr.ToString()); // Prints: OctetString length 19 isHex False: Example OctetString
OctetString.SetRef() method deviates from the method naming to make it as distinctive as possible to reflect the potential danger in setting internal class value by using assignments by reference.
Ability to append data to the existing class value is provided by OctetString.Append() methods. You can append the same data types as you can use with the constructors and Set methods.
Accessing data stored in the OctetString class can be done using the indexed access:
byte[] buf = new byte[] { 0x11, 0xfa, 0xbb, 0xba, 0x00 }; OctetString ostr = new OctetString(buf); for (int i = 0; i < buf.Length; i++) Console.Write("{0}:{1:x2} ", i, buf[i]); Console.WriteLine(""); // Prints: 0:11 1:fa 2:bb 3:ba 4:00
When using indexed access and requesting index position outside of the bounds of the internal byte array of the OctetString class will not result in an Exception being thrown but a value of 0 being returned. This can cause you to process invalid information if not careful.
Second way to access internal OctetString information is to request a copy of the internal byte array using OctetString.ToArray() method. You can also get the same affect by casting an instance of OctetString class to byte[]:
byte[] buf = new byte[] { 0x11, 0xfa, 0xbb, 0xba, 0x00 }; OctetString ostr = new OctetString(buf); byte[] outbuf = ostr.ToArray(); SnmpConstants.DumpHex(outbuf); // Prints: 0000 11 fa bb ba 00 SnmpConstants.DumpHex((byte[])ostr); // Prints: 0000 11 fa bb ba 00
byte[] operator is implicitly defined meaning you can perform any operation you can with a byte array by simply passing the OctetString class to the method without a cast.
To get a string representation of OctetString class value, you can call
For Ethernet MAC address specific functionality, see Snmp#Net EthernetAddress class and its methods.
OctetString class supports comparison operations: ==, !=, CompareTo() and Equals(). CompareTo will compare class contents against a byte array. Equals will compare class contents with value of another OctetString class or a string.
Simple Network Management Protocol defines a number of numeric data types. They can be summarized as:
Signed 32-bit integer derived data types:
Unsigned 32-bit integer derived data type:
Unsigned 64-bit integer derived data type:
TimeTicks SMI value type is also represented as a unsigned 32-bit integer but it manages a time value and is discussed later in this document.
Because of the easy mapping between c# data types and SNMP SMI numeric variable types, mapping between the classes and values they represent is as follows:
| c# type | Snmp#Net class |
|---|---|
| Int32 | Counter32, Integer32 |
| UInt32 | Gauge32, UInteger32 |
| UInt64 | Counter64 |
Each of the numeric data type classes stores internally the specific value type it represents. This value is accessible from outside the class using Value property. Additionally, each numeric value class has an implicit conversion defined to the c# value type it stores.
What this means is that you can perform same operations with the Snmp#Net numeric value class as you can with the base type it represents.
Here is a basic example of some of the available operations:
Counter32 c32 = new Counter32(Int32.MaxValue); Gauge32 g32 = new Gauge32(); g32.Value = UInt32.MaxValue; Counter64 c64 = new Counter64("1234"); Console.WriteLine("Counter32 {0} Gauge32 {1} Counter64 {2}", c32, g32, c64); // Prints: Counter32 2147483647 Gauge32 4294967295 Counter64 1234 Console.WriteLine("Counter32 - 10 = {0}", c32 - 10); // Prints: Counter32 - 10 = 2147483637 UInt64 result = c64 + 100; // Prints: Counter64 + 100 = 1334 Console.WriteLine("Counter64 + 100 = {0}", result);
As you can see from the above example, you can use all Snmp#Net numeric types as regular c# numeric variables.
In the example you can also see that multiple constructors exist. Each class can be initialized with the default value of 0 by calling the base constructor with no arguments. Argument can be value of the specific type represented by the class, a string representation of a numeric value or another instance of the value class for the copy constructor.
Once initialized, each class value can be retrieved or set using the Value property or set using Set() methods.
All numeric value classes support comparison methods Equals() and CompareTo() and comparison operators == and !=. Through implicit conversion to base c# data types, each numeric type class supports all mathematical operations available for the base types.
When polling Counter32 or Counter64 MIB values you have to be careful to check for counter loop which happens when counter reaches the maximum value it can store and loops back to zero. Both 32-bit and 64-bit classes provide static methods Counter32.Diff(Counter32,Counter32) and Counter64.Diff(Counter64,Counter64) that will calculate the difference between the two counter value with counter loop taken into account.
TimeTicks SMI value is a elapsed time value in 100milisecond periods (10 periods per second). It is internally represented as an unsigned 32-bit integer and is derived from the UInteger32 Snmp#Net numeric value class. Through inheritance, TimeTicks class has all the functionality described above related to numeric data type classes.
TimeTicks Snmp#Net class provides a few operations that are specific to the TimeTicks SMI data type. These are:
TimeTicks class has a definition for the explicit conversion to the TimeSpan class to allow for easier manipulation of time periods:
TimeTicks ts = new TimeTicks(1200); Console.WriteLine("TimeTicks: {0}", ts.ToString()); // Prints: TimeTicks: 0d 0h 0m 12s 0ms TimeSpan tspan = (TimeSpan)ts; Console.WriteLine("TimeSpan: {0}", tspan.ToString()); // Prints: TimeSpan: 00:00:12
You can retrieve the total number of milliseconds represented by the class using TimeTicks.Miliseconds property.
Finally, TimeTicks.ToString() will return the time ticks value string representation demonstrated in the example above.
All other methods and properties are the same as in all other unsigned 32-bit integer derived SMI data type classes.