Recipes for extending Net-SNMP
Vincent Bernat
SNMP stands for Simple Network Management Protocol. It allows a manager to query information from an agent. A popular use of SNMP is to retrieve network interface counters to plot a bandwidth graph. While the “S” in SNMP stands for simple, SNMP can be quite difficult to deal with. However, it is still the de facto standard for retrieving metrics in a heterogeneous environment. Net-SNMP is a suite of SNMP applications including an agent. Out of the box, this agent exports a lot of information but if something is missing, there are several ways to extend it. Which ones?
TL;DR
In my opinion, extending Net-SNMP should be done with
either of these methods: using extend
directive, using
pass_persist
directive, and using AgentX protocol.
SNMP in a nutshell#
The variables accessible with SNMP through the agent are organized
in a tree. Each variable is typed and associated to an object
identifier (or OID). For example, .1.3.6.1.2.1.31.1.1.1.1.2
is the
OID of the name of the second network card.
Usually, SNMP is operated over UDP, port 161. For the purpose of this article, we consider a manager can issue three kinds of requests:
- GET
- Retrieve a variable.
- GETNEXT
- Retrieve the next variable, in lexical order of their OID. This operation enables an SNMP manager to walk through all available variables, for example to discover the list of interfaces of an agent. This operation is what makes an SNMP agent difficult to implement but this is also a critical feature.
- SET
- Request the modification of a variable.
Since such an OID may be difficult to handle by humans, available
variables are described by a MIB module which is a file that
should be parsed to get a mapping between OID and variable names and
types. For example, the above OID can also be referred as
IF-MIB::ifName.2
. These MIB modules are described using a subset of
ASN.1 defined in RFC 2578.
A MIB module allows one to group several variables into a conceptual
table. For example, IF-MIB::ifDescr
, IF-MIB::ifType
,
IF-MIB::ifSpeed
are columns of IF-MIB::ifTable
. Each row is
associated to an index. In the case of IF-MIB::ifTable
, it is the
interface index. Net-SNMP includes snmptable
allowing one to display
such a table in a natural manner:1
$ snmptable localhost IF-MIB::ifTable SNMP table: IF-MIB::ifTable ifIndex ifDescr ifType ifMtu ifSpeed 1 lo softwareLoopback 16436 10000000 2 eth0 ethernetCsmacd 1500 100000000 3 eth1 ethernetCsmacd 1500 10000000 4 br0 ethernetCsmacd 1500 0
Variables inside a table are called columnar objects while outside ones are scalar objects. SNMP, as a protocol, does not care of this distinction since it does not know the concept of MIB modules.
Other useful tools from Net-SNMP includes snmpget
to get a specific
variable, snmpwalk
to discover available variables (with GETNEXT
operation) from an OID and snmptranslate
to translate between object
names and OID.
$ snmpget localhost IF-MIB::ifDescr.2 IF-MIB::ifDescr.2 = STRING: eth0 $ snmpwalk localhost IF-MIB::ifDescr IF-MIB::ifDescr.1 = STRING: lo IF-MIB::ifDescr.2 = STRING: eth0 IF-MIB::ifDescr.3 = STRING: eth1 IF-MIB::ifDescr.4 = STRING: br0 $ snmpwalk -On public localhost .1.3.6.1.2.1.2.2.1.2 .1.3.6.1.2.1.2.2.1.2.1 = STRING: lo .1.3.6.1.2.1.2.2.1.2.2 = STRING: eth0 .1.3.6.1.2.1.2.2.1.2.3 = STRING: eth1 .1.3.6.1.2.1.2.2.1.2.4 = STRING: br0 $ snmptranslate -On IF-MIB::ifDescr.3 .1.3.6.1.2.1.2.2.1.2.3
Extending Net-SNMP#
ethtool -S
allows us to access various statistics from a network
card, like the number of octets received and transmitted or the number
of collisions. While some of these statistics are defined in
IF-MIB, in RMON-MIB and in
EtherLike-MIB, some of them are not available and very
specific. Therefore, we would like to export these statistics,
unmodified, through SNMP.
To the best of my knowledge, no MIB module exists for such a usage. The first task is to write one. It can be called ETHTOOL-MIB. It only contains one table indexed by the interface index and the name of the statistic. Here is the expected output:
$ snmpwalk localhost ETHTOOL-MIB::ethtoolStat.2 ethtoolStat.2.'align_errors' = Counter64: 0 ethtoolStat.2.'broadcast' = Counter64: 25 ethtoolStat.2.'multicast' = Counter64: 345 ethtoolStat.2.'rx_errors' = Counter64: 0 ethtoolStat.2.'rx_missed' = Counter64: 0 ethtoolStat.2.'rx_packets' = Counter64: 262011 ethtoolStat.2.'tx_aborted' = Counter64: 0 ethtoolStat.2.'tx_errors' = Counter64: 0 ethtoolStat.2.'tx_multi_collisions' = Counter64: 0 ethtoolStat.2.'tx_packets' = Counter64: 296170 ethtoolStat.2.'tx_single_collisions' = Counter64: 0 ethtoolStat.2.'tx_underrun' = Counter64: 0 ethtoolStat.2.'unicast' = Counter64: 261641
I have coded various implementations of this MIB module and made them available in a Git repository.
With arbitrary commands#
The extend
directive allows the agent to execute an arbitrary
command and to provide its output and status through
NET-SNMP-EXTEND-MIB
module. If we want to export the statistics for
eth0
, we could add something like this in snmpd.conf
, using a
custom script to format the output of ethtool -S
in
a way that is more suitable for our use:
# Export eth0 statistics extend eth0-names /full/path/to/ethtool-stats eth0 names extend eth0-values /full/path/to/ethtool-stats eth0 values
The result can be retrieved through
NET-SNMP-EXTEND-MIB::nsExtendOutput2Table
:
$ snmpwalk localhost NET-SNMP-EXTEND-MIB::nsExtendOutput2Table nsExtendOutLine."eth0-names".1 = STRING: tx_packets nsExtendOutLine."eth0-names".2 = STRING: rx_packets nsExtendOutLine."eth0-names".3 = STRING: tx_errors nsExtendOutLine."eth0-names".4 = STRING: rx_errors nsExtendOutLine."eth0-names".5 = STRING: rx_missed nsExtendOutLine."eth0-names".6 = STRING: align_errors nsExtendOutLine."eth0-names".7 = STRING: tx_single_collisions nsExtendOutLine."eth0-names".8 = STRING: tx_multi_collisions nsExtendOutLine."eth0-values".1 = STRING: 246309 nsExtendOutLine."eth0-values".2 = STRING: 223941 nsExtendOutLine."eth0-values".3 = STRING: 0 nsExtendOutLine."eth0-values".4 = STRING: 0 nsExtendOutLine."eth0-values".5 = STRING: 0 nsExtendOutLine."eth0-values".6 = STRING: 0 nsExtendOutLine."eth0-values".7 = STRING: 0 nsExtendOutLine."eth0-values".8 = STRING: 0
It is also possible to configure such a command using SET
requests. For example, if we want to add statistics for eth2
, we
could issue the following command:
$ snmpset -m +NET-SNMP-EXTEND-MIB localhost \ > 'nsExtendStatus."eth2-names"' = createAndGo \ > 'nsExtendCommand."eth2-names"' = /full/path/to/ethtool-stats \ > 'nsExtendArgs."eth2-names"' = 'eth2 names' \ > 'nsExtendStatus."eth2-values"' = createAndGo \ > 'nsExtendCommand."eth2-values"' = /full/path/to/ethtool-stats \ > 'nsExtendArgs."eth2-values"' = 'eth2 values' nsExtendStatus."eth2-names" = INTEGER: createAndGo(4) nsExtendCommand."eth2-names" = STRING: /full/path/to/ethtool-stats nsExtendArgs."eth2-names" = STRING: eth2 names nsExtendStatus."eth2-values" = INTEGER: createAndGo(4) nsExtendCommand."eth2-values" = STRING: /full/path/to/ethtool-stats nsExtendArgs."eth2-values" = STRING: eth2 values $ snmpgetnext -m +NET-SNMP-EXTEND-MIB localhost \ > 'nsExtendOutLine."eth2-names"' nsExtendOutLine."eth2-names".1 = STRING: tx_packets
This can be very convenient but also a huge security risk. You should disable this functionality or enable it only for some SNMPv3 user.
Using extend
does not allow one to implement an arbitrary MIB module. If
the cache is not disabled, extend
is a good solution for various
simple needs, like providing the content of a one-line file or the
output of some Nagios plugin. However, exporting anything tabular,
like interface statistics, is not a good fit.
With pass-through scripts#
Net-SNMP allows one to extend the agent by delegating some sub-tree to
a script defined by the pass_persist
directive. Such a script will
receive requests on its standard input with a very simple
line-oriented protocol described in the
manual page of snmpd.conf. Here is an example of
interaction:
→ PING ← PONG → get → .1.3.6.1.4.1.39178.100.1.1.1.2.2.109.117.108.116.105.99.97.115.116 ← .1.3.6.1.4.1.39178.100.1.1.1.2.2.109.117.108.116.105.99.97.115.116 ← counter64 ← 223941 → getnext → .1.3.6.1.4.1.39178.100.1.1.1.2.2 ← .1.3.6.1.4.1.39178.100.1.1.1.2.2.97.108.105.103.110.95.101.114 ← counter64 ← 0
The protocol is simple enough to implement it from scratch in any scripting language. However, in Perl, you may prefer to use SNMP::ExtensionSNMP::PassPersist extension from Sébastien Aperghis-Tramoni. You can find the complete example on GitHub.
use SNMP::Extension::PassPersist; my $extsnmp = SNMP::Extension::PassPersist->new( backend_collect => \&update_tree ); $extsnmp->run; sub update_tree { my @interfaces = </sys/class/net/*>; foreach my $interface (@interfaces) { $interface =~ s/^.*\///; open(ETHTOOL, "ethtool -S $interface 2>/dev/null |") or next; while (<ETHTOOL>) { /^\s+(\w+): (\d+)$/ or next; my $name = $1; my $value = int($2); my $oid = oid_compute($interface, $name); $extsnmp->add_oid_entry($oid, "counter", $value); } close(ETHTOOL); } }
With Python, snmp-passpersist module, from Nicolas Agius, provides a similar interface. Look at the complete example on GitHub.
pass_persist
allows you to implement an arbitrary MIB module with
honest performance. Here are some important things to know about this
directive:
- Unless you have a recent version of Net-SNMP, 64-bit types are converted to 32-bit equivalents. The counters may overflow quickly.
- While handling a request, you are not allowed to pause (for example, to fetch a remote information): the agent would become unresponsive. If you need slow-to-get data or data from the agent, grab them in a separate thread and ensure you have them beforehand.
With AgentX protocol#
AgentX is a protocol defined in RFC 2741 allowing a master
agent to be extended by independent sub-agents. For snmpd
to
become a master agent, you just need to add master agentx
in
snmpd.conf
. Here are some of the benefits of an AgentX sub-agent over
pass_persist
:
- No configuration is needed for the master agent to accept an additional sub-agent. A sub-agent registers to the master agent the MIB modules (or part of them) it wants to take care of.
- A sub-agent is decoupled from the master agent. It can run with a different identity or be integrated into another daemon to export its internal metrics, send traps or allow remote configuration through SNMP.
- AgentX protocol can be carried over TCP. Sub-agents can therefore run on a foreign host or in a jailed environment.
- 64-bit types are fully supported. Traps are also supported.
Perl and Python#
Net-SNMP provides a Perl module, Net-SNMP::agent
, to write a
sub-agent using AgentX protocol. This module is not as convenient as
the module for pass_persist
described above since it does not
provide a convenient layer to put required information into some
datastore. It mimics the available C API. Here are a few examples of
its use:
- An implementation of ETHTOOL-MIB I have
done in the same way as the
pass_persist
version. My Perl skills are a bit rusty and the code has many rough edges. - An implementation of a sub-agent for Redis servers.
- An implementation of BRIDGE-MIB shipped with Net-SNMP.
There is no similar binding for Python but there is an
agentx extension using ctypes
. It provides a more
high-level view. I did not write an example for this one but you can
look at the example provided by the author.
C with “new” API#
To write a sub-agent in C, you have to choose between two API:
- the API inherited from UCD-SNMP also known as the “traditional” API;
- the new API developed for Net-SNMP 5.x.
With both API, a handler is registered to take care of one or several sub-trees. With the traditional API, the handler is invoked with very little information. It is then up to you to locate the appropriate variable by mapping the index part of the OID to the objects you want to export. It is quite simple for scalar objects but columnar objects with complex indexes are a pain with such an API.
The new API provides various helpers. Some of them allow you to copy objects into an array or a list and let Net-SNMP library do the hard work of serving them. Another helper will provide methods to serve objects without putting them in a special structure but by providing an iterator. Some of these helpers can also help you to cache some variables.
On top of this new API, Net-SNMP comes with mib2c
, a tool that will
help you convert a MIB module into C code: after a few questions, you
will get a skeleton C code you will have to complete with the logic to
retrieve real objects. A generated file also provides directions on
what to do (with some magic command to have a step-by-step cookbook).
I was able to implement ETHTOOL-MIB
by just running it through
mib2c
, choosing the table container helper with cache and adding
some code in ethtoolStatTable_data_access.c
. You can look at the
result on GitHub. Here is a stripped-down
version2 of what has been modified:
/* Socket for ioctl */ skfd = socket(AF_INET, SOCK_DGRAM, 0); /* Iterate through all interfaces */ getifaddrs(&ifap); for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) { /* Grab statistics name and values */ strcpy(ifr.ifr_name, ifa->ifa_name); drvinfo.cmd = ETHTOOL_GDRVINFO; ifr.ifr_data = (caddr_t) &drvinfo; ioctl(skfd, SIOCETHTOOL, &ifr); n_stats = drvinfo.n_stats; strings->cmd = ETHTOOL_GSTRINGS; strings->string_set = ETH_SS_STATS; strings->len = n_stats; ifr.ifr_data = (caddr_t) strings; ioctl(skfd, SIOCETHTOOL, &ifr); stats->cmd = ETHTOOL_GSTATS; stats->n_stats = n_stats; ifr.ifr_data = (caddr_t) stats; ioctl(skfd, SIOCETHTOOL, &ifr); ifIndex = if_nametoindex(ifa->ifa_name); /* Iterate through statistics */ for (i = 0; i < n_stats; i++) { /* Declare the appropriate index */ strncpy(ethtoolStatName, (char *)&strings->data[i * ETH_GSTRING_LEN], ETH_GSTRING_LEN); ethtoolStatName[sizeof(ethtoolStatName) - 1] = '\0'; ethtoolStatName_len = strlen(ethtoolStatName); /* Append this new variable to the container. */ rowreq_ctx = ethtoolStatTable_allocate_rowreq_ctx(); ethtoolStatTable_indexes_set(rowreq_ctx, ifIndex, ethtoolStatName, ethtoolStatName_len); rowreq_ctx->data.ethtoolStat.high = stats->data[i] >> 32; rowreq_ctx->data.ethtoolStat.low = (uint32_t)stats->data[i]; CONTAINER_INSERT(container, rowreq_ctx); } }
See? It is pretty easy. I have only enumerated all the objects I
wanted to expose. I didn’t even have to convert statistics names to an
OID, mib2c
built a ethtoolStatTable_indexes_set()
for this
purpose. OpenHPI project provides an
extensive documentation to write a sub-agent this
way.
Now, on the downside, to implement a single table, I have 14
generated files whose code style is usually unfit to integrate
into an existing project: some people, I included, do not like
automatically generated code, all the more when it is so verbose. The
solution would be to use the new API without mib2c
; unfortunately,
while the documentation exists, all provided examples
rely exclusively on mib2c
tool. Unless you are able to grasp the new
API without it, I would restrict its use to two cases:
- Writing a new module for inclusion into Net-SNMP. Since most
modules are now written with the help of
mib2c
, there is no coding style problem. - Writing a standalone sub-agent for some MIB. As a standalone project, you won’t run into coding style problems either.
For other uses, let’s have a look at the traditional API.
C with “traditional” API#
The traditional API is way simpler and even if it is quite old, it is easier to find associated examples. Here is how to initialize the agent, again, in a stripped-down version (the complete version is on GitHub):
static oid ethtool_oid[] = {1, 3, 6, 1, 4, 1, 39178, 100, 1}; static struct variable3 ethtool_vars[] = { {1, ASN_COUNTER64, RONLY, ethtool_stat, 3, {1, 1, 2}} }; int main() { netsnmp_enable_subagent(); snmp_disable_log(); snmp_enable_stderrlog(); init_agent("ethtoolAgent"); REGISTER_MIB("ethtoolStatTable", ethtool_vars, variable3, ethtool_oid); init_snmp("ethtoolAgent"); while (1) agent_check_and_process(1); }
The REGISTER_MIB()
macro is used to register the provided root OID
(ethtool_oid
) to the master agent. It also registers handlers
associated to various sub-OID through the ethtool_vars[]
array. Each
member of this array features the following fields:
magic
is an integer allowing one to discriminate OID handled by the same handler. For a table, this allows one to handle several columns with the same handler since they share a common index.type
is the type of the variable that will be returned.acl
tells if the object is read-only (RONLY
) or read-write (RWRITE
).findVar
is the handler associated to the object. Its task will be to find the appropriate object and return its value.- The two last fields are the relative OID this entry is responsible for along with its length.
The prototype of ethtool_stat()
handler function is the following:
static u_char* ethtool_stat(struct variable *vp, oid *name, size_t *length, int exact, size_t *var_len, WriteMethod **write_method);
This function should return the value of the requested variable or
NULL
if no appropriate object was found. The value can be statically
stored since it will be copied. Here is the description of the
arguments:
struct variable *vp
is the structure the handle was associated with, except thatvp->name
is now a full OID, not a relative one. You can accessvp->magic
to determine which column or which scalar to handle if this handler is common to several objects.oid *name
andsize_t *length
describe the OID requested. If you want to return another OID (for a GETNEXT request for example), you can modify them.name
points to an OID array large enough to fit any OID you want to return.int exact
tells if the requested OID should be exactly matched (case of GET or SET) or not (GETNEXT).size_t *var_len
is used to tell the size of the returned object.WriteMethod **write_method
is only used if the variable is modifiable. When a SET request is received, the API will call the handler withexact
set to 1 and expects to get a pointer to the function that will handle the write operation.3
To implement this function, you are on your own. When relying on externally fetched data, you need to handle a cache yourself. I have used a red-black tree for this purpose (with code stolen from OpenBSD). This also enables an easy implementation of the GETNEXT operation.
Here are three projects using this API to provide SNMP support:
-
Keepalived is a VRRP daemon and a monitoring daemon for LVS clusters. I have added a complete SNMP support. The most interesting files are
core/snmp.c
,check/check_snmp.c
andvrrp/vrrp_snmp.c
. It features a tight integration into Keepalived event loop that should be reusable by other projects. It includes read-only support of all data structures (configuration, state and statistics), ability to send traps on events and a write support for some values. -
lldpd is an implementation of 802.1AB (LLDP) which is a protocol allowing an equipment to advertise itself to its neighbors on Ethernet level. 802.1AB comes with a huge MIB module and many extensions. lldpd provides a fairly complete read-only implementation of this MIB module. Some tables have complex indexes. Have a look at agent.c. There is currently no write support and no traps but lldpd features privilege separation and some code allows it to still use a Unix socket to speak with the master agent.
-
Asterisk, an open source PBX, includes an AgentX sub-agent allowing one to grab some metrics. Look at
res/snmp/agent.c
.
Update (2012-03)
If it is critical for your project to not block, you should be aware that using Net-SNMP implementation of AgentX has some issues. Have a look at my other post on this topic.
Other solutions#
The manual page for snmpd.conf explores other ways to extend Net-SNMP agent under the section “Extending agent functionality”:
exec
andsh
are deprecated. Useextend
instead.pass
is a bit simpler thanpass_persist
. The command is run once for each request. Such a command can easily be turned into apass_persist
script. Moreover, Net-SNMP will not answer any request while apass
command is running.proxy
redirects requests to another SNMP agent. Unless you have some code that can only act as a complete SNMP agent, it is better to use AgentX instead. For programs using Net-SNMP API, turning an agent into an AgentX sub-agent is just one line of code. Since a sub-agent involves less code than a complete agent, it is more efficient and more reliable.smux
has been superseded by AgentX.perl
anddlmod
allow one to load and execute external code inside the running agent. Unless you have important performance requirements, it is better to use a sub-agent instead: if some bug happens, only the sub-agent will be affected, not the master agent. A sub-agent sticks more closely to the Unix philosophy: “write programs that do one thing and do it well.” However, Net-SNMP FAQ says:
Implementing the module in C within the main agent (directly or via
dlmod
) is probably the most efficient and reliable, closely followed by embedded perl (or python) extensions. These have the advantage of minimal overheads between the code implementing the MIB module, and the agent framework, and no inter-process communication issues.
-
Additional arguments for
snmp*
are usually required to specify a version and a community (for SNMPv1 and SNMPv2c) or a user (for SNMPv3). In all examples, I do not specify anything. I assume that~/.snmp/snmp.conf
contains the appropriate bits. Moreover, some outputs are truncated. ↩︎ -
No allocations, no variable declarations and no error checkings. Do not do this at home! ↩︎
-
If you want to look at an example of how to handle a SET request, look at the SNMP support for Keepalived. A SET operation is more complex because SNMP ensures that the SET operation is atomic across all provided variables. It uses a three-staged commit. The traditional API is very inefficient for this kind of operation because the object has to be located again for each step. ↩︎