Again with the Star Wars quotes. I was trying to find a quote related to SNMP, but sadly George Lucas didn’t include SNMP concepts in any of the movies. I thought about going with ‘You will never find a more wretched hive of scum and villainy’. Given my experience over the years with the vagaries of SNMP, I thought this quote would be fitting. However, upon recollecting the joys of Return of the Jedi, I realised that Admiral Ackbar was all over the SNMP lingo and was in fact trying to tell a joke on the bridge of his ship, Home One, about what the SNMP agent said to the SNMP server… Given he managed to time the punch line to their arrival from hyperspace – genius.
Anyway, unlike Star Wars, SNMP has sadly not improved substantially over time. I was writing a network asset discovery tool for populating my home CMDB (as you do), and I was looking for a relatively easy way to send SNMP queries from a C/C++ Windows Service that I had created without having to resort to using a third-party library, or writing an implementation of the SNMP protocol myself. Despite the misleading nature of the name, Simple Network Management Protocol can be relatively complex to use due to vendor differences in implementations, and the lack of solid documentation on said implementations.
This entry will hopefully shed some light on how to send SNMP queries using nothing but components included with the OS…. and of course a device to query, some C/C++ code, and a sense of adventure.
As a quick recap on how the SNMP protocol works, SNMP is made up of several components. The SNMP agent, which runs on the managed devices, and the system/tool/application that interacts with these agents – the network management system (NMS). SNMP provides two primary functions.
The first function provides the ability to extract information from a device. This information could be the host name, OS type, disk usage, arp table, etc. This function is predominately used for monitoring network devices (CPU/Memory utilisation, interface status, traffic throughput), and for performing asset discovery (what devices exist on my network). In fact, some of the new mini electronics boards that you can buy these days, like the arduino boards, allow you to extract information from them via SNMP, for example extracting information from a temperature/humidity sensor.
The second function provides the ability for a device to send events, or as SNMP calls them, traps, to the network management system. The traps are received by the NMS indicating an event has occurred, for example the state of an interface has changed. If the earlier SNMP joke didn’t make sense before, hopefully this has provided some context and humour. In fact, I’m still recovering from the hilarity of the gag myself. Ah quotes…
For there to be a common language used between the agent and the NMS, each object that can be queried must be assigned an Object Identifier (OID). These OIDs are documented in a Management Information Base (MIB) file. There are standard MIBs for SNMP, and of course vendor specific MIBs. Typically, if you were using an NMS to monitor your Juniper firewall, you would go to the Juniper website, locate the MIBs relevant to your device and then import them into the NMS software. This would allow the software to more easily understand what information can be extracted, and what format that information is in.
The function that I’m going to focus on is that of the SNMP query. Microsoft Windows Server has always included an SNMP agent (I’d be surprised if the code has changed greatly over the last 15 years, other than the inclusion of IPv6, WMI and SNMPv3). There used to be a Win32 API set for interacting with this agent, however upon further investigation, I found that there was also now an SNMP WMI provider provided with the OS. So I used the Add Feature wizard on my Windows 2008 R2 Development server to install both the SNMP Agent and the SNMP WMI Provider. The SNMP Agent you can see – it creates a Windows System service. The WMI provider exists happily in the background.
After having installed the agent, the next item that needs to be considered is the importing of the relevant MIB files into the WMI repository so that the WMI subsystem can accurately match WQL queries to SNMP OIDs. This only needs to be done once, and consequently can be built into an install mechanism for the application/service.
To start with, I chose the MIB_II MIB (mib_ii.mib), also known as the RFC 1213 MIB. This was a good starting place as it included some basics such as the host name, description and an interface listing. I also used the HOST RESOURCES MIB (hostmib.mib). Both of these files were located on Windows in the Windows\System32 folder, because thats clearly the location for randomly placing MIB files. Any other MIBs that you’re looking for can be found in numerous places online.
To import these files, we need to use several built in tools, SMI2SMIR and MOFCOMP.
SMI2SMIR /G hostmib.mib > hostmib.mof
This command would have taken the hostmib.mib file, and then created an appropriate MOF file (Managed Object Format). Or alternatively given you a lot of errors indicating it was missing dependent MIB files. Some MIB files will refer to other MIBs as you can see in the IP-MIB example below.
IP-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE,
Integer32, Counter32, IpAddress,
mib-2, Unsigned32, Counter64,
zeroDotZero FROM SNMPv2-SMI
PhysAddress, TruthValue,
TimeStamp, RowPointer,
TEXTUAL-CONVENTION, TestAndIncr,
RowStatus, StorageType FROM SNMPv2-TC
MODULE-COMPLIANCE, OBJECT-GROUP FROM SNMPv2-CONF
InetAddress, InetAddressType,
InetAddressPrefixLength,
InetVersion, InetZoneIndex FROM INET-ADDRESS-MIB
InterfaceIndex FROM IF-MIB;
If I was trying to import the IP-MIB, I would also need to acquire the IF-MIB, INET-ADDRESS-MIB etc, and any MIBs that they themselves reference. It practice, it doesn’t take long to track them all down. However, it does mean that I would need to include these dependencies when I’m using SMI2SMIR.
SMI2SMIR /G IP-MIB.mib IF-MIB.mib INET-ADDRESS-MIB.mib > file.mof
Now that we have the MOF files, they need to be imported into the WMI repository.
MOFCOMP hostmib.mof
MOFCOMP file.mof
With that action complete, we now have the SNMP Agent and SNMP WMI Provider installed. We now also have the required MIBs loaded into the WMI Repository. All we need to do now is create the query code. WMI is quite a handy interface as the code that we will use to query the WMI SNMP provider is very similar to the code we would write to use other WMI Providers to query information from Windows systems. This code example will be in C, but the concepts are very similar in any .NET language, or script.
Let’s start with the include files. All of the information on the Win32 API in question, the include files and the library files are contained in the MSDN documentation library.
#include <windows.h> #include <wbemidl.h> #include <comdef.h>
Then there’s the library file.
#pragma comment(lib, "wbemuuid.lib")
After that, it’s over to the code. Even in C++, I choose to declare all of my variables at the top of a function, and not mix them randomly throughout the function. Having said that, to make the example easier to follow, I’ll declare the variable the first time it is used, so that you can better understand it’s purpose and keep track of where they are referenced.
In this first section, we need to initialise the COM subsystem. I’ve included the function call, and then the following cleanup code. The Logging_LogSystem and Support_HandleNonSystemError are other utility functions of mine, that act essentially like the printf command, but do more than just print to stdout.
HRESULT hResult; int iRetVal = TRUE; hResult = CoInitializeEx(0, COINITBASE_MULTITHREADED); if (FAILED(hResult)) { iRetVal = FALSE; Logging_LogSystem("Unable to initialize COM subsystem.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "CoInitializeEx()", "hResult: %X", hResult); goto Cleanup; } hResult = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); if (FAILED(hResult)) { iRetVal = FALSE; Logging_LogSystem("Unable to initialize COM subsystem security.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "CoInitializeSecurity()", "hResult: %X", hResult); goto Cleanup; } Cleanup: CoUninitialize();
Now we need to start instantiating some COM objects. The Locator object is the first object we need when talking to WMI. The Context object is needed to pass in configuration parameters – in our case, the SNMP target, SNMP community string, and SNMP version information.
IWbemLocator *pLocator = NULL; IWbemContext *pContext = NULL; hResult = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID *) &pLocator); if (FAILED(hResult)) { iRetVal = FALSE; Logging_LogSystem("Unable to create locator instance.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "CoCreateInstance()", "hResult: %X", hResult); goto Cleanup; } hResult = CoCreateInstance(CLSID_WbemContext, 0, CLSCTX_INPROC_SERVER, IID_IWbemContext, (LPVOID *) &pContext); if (FAILED(hResult)) { iRetVal = FALSE; Logging_LogSystem("Unable to create context instance.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "CoCreateInstance()", "hResult: %X", hResult); goto Cleanup; } Cleanup: if (pContext) pContext->Release(); if (pLocator) pLocator->Release();
Now that we have the context object, we need to pass in the configuration parameters. As my application is compiled in Multi-Byte mode, as opposed to the default Unicode mode, I need to convert my character strings to wide character strings. I’ll just insert the code here for the SNMP target device, as the community string code will be the same. The code to configure the SNMP version however will be smaller as I’m hard coding the SNMP version setting (it defaults to SNMP v1 – pfft). As always, whenever you’re working with input parameters they should be validated for length and data type first. I’ll keep these examples to the point however.
wchar_t wszString[MAX_PATH]; VARIANT vValue; if (MultiByteToWideChar(CP_THREAD_ACP, 0, szTargetIP, -1, wszString, sizeof(wszString)) == 0) { iRetVal = FALSE; Logging_LogSystem("Unable to convert Target IP to unicode.\r\n"); Support_HandleError(__FUNCTION__, "MultiByteToWideChar()", GetLastError()); goto Cleanup; } VariantInit(&vValue); vValue.vt = VT_BSTR; vValue.bstrVal = SysAllocString(wszString); pContext->SetValue(L"AgentAddress", 0, &vValue); SysFreeString(vValue.bstrVal); VariantClear(&vValue); // Use the above code to also set the AgentReadCommunityName value. VariantInit(&vValue); vValue.vt = VT_BSTR; vValue.bstrVal = SysAllocString(L"2C"); // Version 2C pContext->SetValue(L"AgentSNMPVersion", 0, &vValue); SysFreeString(vValue.bstrVal); VariantClear(&vValue);
So, let’s recap. We’ve created the Locator object, created the Context object and addded the target SNMP device, version and read community string. Now we need to connect to the local SNMP provider, and most importantly, set the proxy blanket. What a fun name. Proxy blanket. I’m trying to imagine what a blanket by proxy would look like…
IWbemServices *pService = NULL; BSTR bstrServer = NULL; bstrServer = SysAllocString(L"root\\snmp\\localhost"); hResult = pLocator->ConnectServer(bstrServer, NULL, NULL, 0, NULL, 0, 0, &pService); SysFreeString(bstrServer); if (FAILED(hResult)) { iRetVal = FALSE; Logging_LogSystem("Unable to connect to local SNMP service.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "ConnectServer()", "hResult: %X", hResult); goto Cleanup; } hResult = CoSetProxyBlanket(pService, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); if (FAILED(hResult)) { iRetVal = FALSE; Logging_LogSystem("Unable to configure proxy.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "CoSetProxyBlanket()", "hResult: %X", hResult); goto Cleanup; } Cleanup: if (pService) pService->Release();
Well, as much as I can honestly say that was a lot of fun, now comes the really fun part. Executing out first WQL SNMP query. I hesitate to say ‘our first WQL SNMP query’, as that usually implies impending code with the ubiquitous and terribly annoying ‘Hello World’ phrase. WQL, or WMI Query Language is an SQL type language that you can use to query WMI providers. The syntax of the query is made up of several parts. The SELECT * requests that all fields/attributes are retrieved for each entry (a lot of SNMP queries return a table, or multiple rows/entries). The FROM SNMP_RFC1213_MIB_system is actually three different elements joined by underscores. Element 1 is the fixed word SNMP – that one should be obvious. Element 2 describes the name of the requested MIB. In our case, RFC1213_MIB – this is actually the name of the MIB from the mib_ii.mib file. However, if you open that file you’ll notice that the name of the MIB is actually RFC1213-MIB (RFC1213-MIB DEFINITIONS ::= BEGIN). The WMI provider requires that you convert the hyphens into underscores when executing a query. Good to know. Element 3 indicates which OID in the MIB you are requesting. In our case, we’re looking to extract the system OID.
SELECT * FROM SNMP_RFC1213_MIB_system
Time for the query.
BSTR bstrWQL = NULL, bstrQuery = NULL; bstrWQL = SysAllocString(L"WQL"); bstrQuery = SysAllocString(L"SELECT * FROM SNMP_RFC1213_MIB_system"); hResult = pService->ExecQuery(bstrWQL, bstrQuery, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, pContext, &pEnumerator); SysFreeString(bstrWQL); SysFreeString(bstrQuery); if (FAILED(hResult)) { iRetVal = FALSE; Logging_LogSystem("Unable to execute query.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "ExecQuery()", "hResult: %X", hResult); goto Cleanup; } Cleanup: if (pEnumerator) pEnumerator->Release();
With the query hopefully executed successfully at this point, we now need to iterate through the result set. Here, we’re extracting the sysDescr, sysName and sysUptime OIDs. We also release the pEnumerator object here as there will likely be additional queries and I don’t want to wait until the Cleanup label to release the object. I then need to set pEnumerator to NULL so that when the Cleanup code does execute, it doesn’t try and execute Release() on an already released object. Funny story, the Release() function always makes me think of the Life of Brian. Release Roderick!
As a side note, whoever teaches coders not to use the goto command is crazy. As always, the objective of teaching is not to provide non-flexible rules, but rather to show situations where the rule makes sense, and situations where they don’t so that the coder can apply their own judgement to the situation. Rant over, but its a little frustrating. I find the goto command particularly useful for handling cleanup code without me having to repeat it all through a function. Now trying to use goto instead of subroutines and functions, well that is crazy.
IEnumWbemClassObject *pEnumerator = NULL; IWbemClassObject *pObject = NULL; ULONG uResult = 0; while (pEnumerator) { hResult = pEnumerator->Next(WBEM_INFINITE, 1, &pObject, &uResult); if (uResult == 0) break; hResult = pObject->Get(L"sysDescr", 0, &vValue, 0, 0); if (FAILED(hResult)) { Logging_LogSystem("Unable to retrieve query result.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "Get() sysDescr", "hResult: %X", hResult); } else { Logging_LogSystem("sysDescr: %S\r\n", vValue.bstrVal); } hResult = pObject->Get(L"sysUptime", 0, &vValue, 0, 0); if (FAILED(hResult)) { Logging_LogSystem("Unable to retrieve query result.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "Get() sysUptime", "hResult: %X", hResult); } else { Logging_LogSystem("sysUptime: %d\r\n", vValue.iVal); } hResult = pObject->Get(L"sysName", 0, &vValue, 0, 0); if (FAILED(hResult)) { Logging_LogSystem("Unable to retrieve query result.\r\n"); Support_HandleNonSystemError(__FUNCTION__, "Get() sysName", "hResult: %X", hResult); } else { Logging_LogSystem("sysName: %S\r\n", vValue.bstrVal); } pObject->Release(); } pEnumerator->Release(); pEnumerator = NULL;
And that’s it. That code will use WMI to instruct the local SNMP agent to query the specified target device using the supplied community string for the host name, up time and system description. I’ll include a couple of additional queries (for ipAddrTable and ifTable) in the example code file to collect some basic networking information. Hopefully though at this point, there’s enough information there for you to get started. Just remember though:
- Install the SNMP Agent and the SNMP WMI Provider.
- Convert the MIBs into MOF files.
- Import the MOF files into the WMI Repository.
- Provide the SNMP target host name/IP address and read community string values to the WMI provider.
- Execute the query.
I’ve uploaded an example source file with the above code here.
Once this is in place, the fun is then working with the different vendor devices where they behave a little differently for the same request. As always, flexibility in the code, and inspect the data you’re getting back before deciding how to process it. The easiest way to break an application is for the coder to assume integrity of the incoming data.
Once again, I hope this helps, I was most pleased when I started pulling real data from devices in my discovery service.
~ Mike
Thank you Mike!!!!! Yours is the most complete, most relevant example I’ve found in 2 weeks of looking everywhere – you rock!
Brilliant thanks.
Nice tutorial! Would this work with .mib files for an APC UPS and would SNMPv3 be a valid version?
Thanks Alex. Any correctly formatted MIB should be fine, and wouldn’t expect v3 to be an issue. Good luck.