snmpserver_web_logo

SnmpServer C++ library

v1.0.0

Table of contents

Overview

SnmpServer is a C++ library that implements an SNMP agent: an SNMPv2c and SNMPv3 command responder and notification originator. The application registers scalar objects, tables and notifications, and the library answers the GET, GETNEXT, GETBULK and SET requests of SNMP managers and sends notifications to the configured targets. Main features:

SnmpServer is general-purpose software: it is not tied to any sector or application, and the developer of a program decides what the program does with it.

  • SNMPv2c (RFC 3416) and SNMPv3 (RFC 3412) with the User-based Security Model (USM, RFC 3414): authentication with HMAC-SHA-2 (RFC 7860), encryption with AES (RFC 3826), engine discovery, and creation, key change and deletion of users by a manager (usmUserTable).
  • View-based Access Control Model (VACM, RFC 3415) for SNMPv2c communities and SNMPv3 users.
  • Scalar objects whose values are held by the library (updated by the application with setValue(…)) or read and written by handlers of the application.
  • Conceptual tables with handlers; the library encodes the INDEX values into instance OIDs (RFC 2578).
  • Real values sent as scaled integers with a declared decimal scale and UNITS.
  • Notifications: SNMPv2 traps and InformRequests with retransmission and statistics.
  • Standard MIB objects: SNMPv2-MIB (RFC 3418), SNMP-FRAMEWORK-MIB, SNMP-MPD-MIB, the USM and VACM MIB tables, and optionally snmpCommunityTable (RFC 3584).
  • Generation of the SMIv2 MIB module (RFC 2578, RFC 2579, RFC 2580) that describes the registered objects and notifications.
  • UDP transport over IPv4 and IPv6, with one socket per address family.
  • Optional FIPS 140-3 operation through the OpenSSL FIPS provider.

The library is cross-platform (Linux and Windows) and uses the C++17 standard. The public interface is the SnmpServer class declared in SnmpServer.h and the data structures declared in SnmpServerTypes.h; both headers include only standard C++ headers. The library has its own BER codec and UDP transport. One worker thread of the agent answers requests and calls the handlers of the application.

SnmpServer depends on the CryptoProvider library for SNMPv3: SHA-2, HMAC, AES and random numbers are calls of CryptoProvider, and SnmpServer contains no cryptographic implementation of its own. CryptoProvider uses OpenSSL 3 or Windows CNG (on Linux without OpenSSL 3 it uses its own standalone implementations). A build without SNMPv3 (${PARENT}_SNMPSERVER_WITH_V3 OFF) needs no CryptoProvider. SnmpServer does not contain CryptoProvider: in a stand alone build it is expected in the folder next to SnmpServer, and a parent project adds it (see Build and connect to your project).

Versions

Table 1 - Library versions.

Version Release date What’s new
1.0.0 29.10.2026 - First release.

Library files

The library is supplied as source code only. The user receives a set of files in the form of a CMake project (repository). SnmpServer.h and SnmpServerTypes.h are the headers an application includes (SnmpServer.h includes SnmpServerTypes.h). CMake generates src/SnmpServerVersion.h from src/SnmpServerVersion.h.in with the version of the project() command in src/CMakeLists.txt. Everything in src/internal is internal implementation and is not part of the API. The repository structure is shown below:

CMakeLists.txt ---------------------- Main CMake file of the library.
README.md --------------------------- Documentation of the library.
static ------------------------------ Folder with images of the documentation.
    snmpserver_web_logo.png --------- Logo of the library.
src --------------------------------- Folder with library source code.
    CMakeLists.txt ------------------ CMake file of the library.
    SnmpServer.h -------------------- Main library header file (SnmpServer class).
    SnmpServerTypes.h --------------- Header file with the data structures.
    SnmpServer.cpp ------------------ C++ implementation file of the SnmpServer class.
    SnmpServerVersion.h ------------- Header file with library version (generated).
    SnmpServerVersion.h.in ---------- File for CMake to generate version header.
    internal ------------------------ Folder with internal implementation (not part of the API).
        Ber.h ----------------------- BER tags, errors and size calculation.
        Ber.cpp --------------------- Implementation of Ber.h.
        BerReader.h ----------------- BER decoder with bounds checks.
        BerReader.cpp --------------- Implementation of BerReader.h.
        BerWriter.h ----------------- BER encoder.
        BerWriter.cpp --------------- Implementation of BerWriter.h.
        CommunityTable.h ------------ snmpCommunityTable (RFC 3584).
        CommunityTable.cpp ---------- Implementation of CommunityTable.h.
        EngineClock.h --------------- snmpEngineBoots and snmpEngineTime.
        EngineClock.cpp ------------- Implementation of EngineClock.h.
        EngineId.h ------------------ snmpEngineID: construction, check and hex form.
        EngineId.cpp ---------------- Implementation of EngineId.h.
        EngineStore.h --------------- Persistent storage of the engine ID and boot counter.
        EngineStore.cpp ------------- Implementation of EngineStore.h.
        MessageCodec.h -------------- SNMPv2c message codec.
        MessageCodec.cpp ------------ Implementation of MessageCodec.h.
        MibEmitter.h ---------------- SMIv2 MIB module generator.
        MibEmitter.cpp -------------- Implementation of MibEmitter.h.
        MibNode.h ------------------- Node interface and value types of the MIB registry.
        MibScalar.h ----------------- Scalar object of the MIB registry.
        MibScalar.cpp --------------- Implementation of MibScalar.h.
        MibTable.h ------------------ Table of the MIB registry: index encoding, RowStatus.
        MibTable.cpp ---------------- Implementation of MibTable.h.
        MibText.cpp ----------------- Checks of SMIv2 texts (descriptors, timestamps).
        MibTree.h ------------------- MIB registry ordered by OID.
        MibTree.cpp ----------------- Implementation of MibTree.h.
        MibValueCodec.h ------------- Conversion between registry values and wire values.
        MibValueCodec.cpp ----------- Implementation of MibValueCodec.h.
        NotificationOriginator.h ---- Traps and informs, retransmission, counters.
        NotificationOriginator.cpp -- Implementation of NotificationOriginator.h.
        Oid.h ----------------------- Object identifier type.
        Oid.cpp --------------------- Implementation of Oid.h.
        PduCodec.h ------------------ PDU codec shared by SNMPv2c and SNMPv3.
        PduCodec.cpp ---------------- Implementation of PduCodec.h.
        RequestDispatch.h ----------- Processing of GET, GETNEXT, GETBULK and SET.
        RequestDispatch.cpp --------- Implementation of RequestDispatch.h.
        RowStatus.h ----------------- RowStatus state machine (RFC 2579).
        RowStatus.cpp --------------- Implementation of RowStatus.h.
        SetTransaction.h ------------ All-or-nothing SET processing (RFC 3416).
        SetTransaction.cpp ---------- Implementation of SetTransaction.h.
        SnmpMessage.h --------------- Decoded SNMP message.
        Snmpv2Mib.h ----------------- SNMPv2-MIB objects (RFC 3418).
        Snmpv2Mib.cpp --------------- Implementation of Snmpv2Mib.h.
        Snmpv3Codec.h --------------- SNMPv3 message codec (RFC 3412).
        Snmpv3Codec.cpp ------------- Implementation of Snmpv3Codec.h.
        Snmpv3Message.h ------------- Decoded SNMPv3 message.
        Snmpv3Mib.h ----------------- SNMP-FRAMEWORK-MIB, SNMP-MPD-MIB and usmStats objects.
        Snmpv3Mib.cpp --------------- Implementation of Snmpv3Mib.h.
        Snmpv3Report.h -------------- SNMPv3 Report PDU.
        Snmpv3Report.cpp ------------ Implementation of Snmpv3Report.h.
        Snmpv3Responder.h ----------- Response to an accepted SNMPv3 request.
        Snmpv3Responder.cpp --------- Implementation of Snmpv3Responder.h.
        TestAndIncr.h --------------- TestAndIncr textual convention (RFC 2579).
        TestAndIncr.cpp ------------- Implementation of TestAndIncr.h.
        UdpLink.h ------------------- UDP socket.
        UdpLink.cpp ----------------- Implementation of UdpLink.h.
        Usm.h ----------------------- USM receive processing (RFC 3414).
        Usm.cpp --------------------- Implementation of Usm.h.
        UsmCrypto.h ----------------- USM cryptography through CryptoProvider, FIPS.
        UsmCrypto.cpp --------------- Implementation of UsmCrypto.h.
        UsmUserTable.h -------------- usmUserTable and usmUserSpinLock.
        UsmUserTable.cpp ------------ Implementation of UsmUserTable.h.
        UsmUsers.h ------------------ USM users and their localized keys.
        UsmUsers.cpp ---------------- Implementation of UsmUsers.h.
        Vacm.h ---------------------- VACM access control decision (RFC 3415).
        Vacm.cpp -------------------- Implementation of Vacm.h.
        VacmTables.h ---------------- SNMP-VIEW-BASED-ACM-MIB tables.
        VacmTables.cpp -------------- Implementation of VacmTables.h.
test -------------------------------- Folder with tests.
    CMakeLists.txt ------------------ CMake file of the tests.
    AgentLoop.h --------------------- Minimal agent assembly for the tests.
    EngineIdParse.h ----------------- snmpEngineID parsing for the tests.
    KeyChangeOracle.h --------------- Manager side of KeyChange for the tests.
    TestBackend.h ------------------- Capabilities of the cryptographic backend.
    TestNoDialogs.h ----------------- Suppresses error dialogs of aborting tests.
    TestProcess.h ------------------- Running of external programs from tests.
    TestRuntime.h ------------------- Run-time constants for the tests.
    TestSocket.h -------------------- Socket functions for the tests.
    TestAgent.cpp ------------------- Read-only SNMPv2c agent for manual checks.
    TestAuthenticationFailure.cpp --- authenticationFailure notifications.
    TestBer.cpp --------------------- BER codec.
    TestCommunityTable.cpp ---------- snmpCommunityTable.
    TestDispatch.cpp ---------------- Request processing.
    TestEngine.cpp ------------------ Engine ID, boots and time.
    TestEngineStore.cpp ------------- Persistent boot counter.
    TestFips.cpp -------------------- FIPS operation.
    TestFuzz.cpp -------------------- Decoding of damaged messages.
    TestIntegration.cpp ------------- Agent and external SNMP command line tools.
    TestKeyChange.cpp --------------- KeyChange key rotation.
    TestKeyWipe.cpp ----------------- Wiping of key material.
    TestLegacyAlgorithms.cpp -------- Absence of MD5, SHA-1 and DES.
    TestListeners.cpp --------------- Several listeners.
    TestMessage.cpp ----------------- SNMPv2c message codec.
    TestMib.cpp --------------------- MIB registry.
    TestMibEmitter.cpp -------------- MIB module generator.
    TestNotification.cpp ------------ Traps and informs.
    TestOutOfMemory.cpp ------------- Memory allocation failures.
    TestPortableAgent.cpp ----------- One request over a socket on every platform.
    TestPublicApi.cpp --------------- Public interface.
    TestRowStatus.cpp --------------- RowStatus state machine.
    TestSet.cpp --------------------- SET transaction.
    TestSetValue.cpp ---------------- Values refused by setValue(...).
    TestSnmpv2Mib.cpp --------------- SNMPv2-MIB objects.
    TestSnmpv3Codec.cpp ------------- SNMPv3 message codec.
    TestSnmpv3Fit.cpp --------------- Response size rules of SNMPv3.
    TestSnmpv3Report.cpp ------------ Report PDU and discovery.
    TestStopFromHandler.cpp --------- stop() called from a handler.
    TestThreads.cpp ----------------- Calls from several threads.
    TestThrowingHandler.cpp --------- Handlers that throw exceptions.
    TestUdpLink.cpp ----------------- UDP socket.
    TestUnit.cpp -------------------- Object identifier type.
    TestUsm.cpp --------------------- USM receive processing.
    TestUsmCrypto.cpp --------------- Key localization and digests.
    TestUsmPrivacy.cpp -------------- AES privacy.
    TestUsmUserTable.cpp ------------ usmUserTable.
    TestVacm.cpp -------------------- VACM decision.
    TestVacmTables.cpp -------------- VACM tables.
    TestWorkerBarrier.cpp ----------- Exceptions in the worker thread.
    TestWritableTable.cpp ----------- Writable table with RowStatus.
    valgrind.supp ------------------- Suppression file of a memory checker.
example ----------------------------- Folder with the example.
    CMakeLists.txt ------------------ CMake file of the example.
    main.cpp ------------------------ Source code of the example.
SnmpServerDeviceTemplate ------------ Folder with the device template program.
    CMakeLists.txt ------------------ CMake file of the device template.
    main.cpp ------------------------ Source code of the device template.

SNMP support

Table 2 - Supported SNMP features.

Feature RFC Support
SNMPv2c requests: GET, GETNEXT, GETBULK, SET 3416 Yes
SNMPv2 traps 3416 4.2.6 Yes
InformRequest (confirmed notifications) 3416 4.2.7 Yes, retransmitted until acknowledged (see Confirmed notifications)
SNMPv1 1157 No
SNMPv3 message processing 3412 Yes (build option ${PARENT}_SNMPSERVER_WITH_V3, ON by default)
USM authentication 3414, 7860 HMAC-SHA-224, HMAC-SHA-256, HMAC-SHA-384 and HMAC-SHA-512. HMAC-MD5-96 and HMAC-SHA-96 of RFC 3414 are not supported.
USM privacy 3414, 3826 AES-128; AES-192 and AES-256 with conditions (see Limitations). DES-CBC of RFC 3414 is not supported.
USM engine discovery and timeliness 3414 3.2 Yes
usmUserTable: creation, key change and deletion of users by a manager 3414 5 Yes
usmUserSpinLock and the six usmStats counters 3414 5 Yes
VACM access control decision 3415 3.2 Yes
vacmSecurityToGroupTable 3415 4 Yes, read-create
vacmAccessTable, vacmViewTreeFamilyTable 3415 4 Read-only (see Limitations)
SNMPv2-MIB: system and snmp groups 3418 Yes
SNMP-FRAMEWORK-MIB: snmpEngineGroup 3411 5 Yes: snmpEngineID, snmpEngineBoots, snmpEngineTime, snmpEngineMaxMessageSize
SNMP-MPD-MIB: snmpMPDGroup 3412 5 Yes: snmpUnknownSecurityModels, snmpInvalidMsgs, snmpUnknownPDUHandlers
snmpCommunityTable 3584 5 Yes, read-only, off by default (see snmpCommunityTable)
RowStatus row creation and deletion 2579 7.1 Yes, in usmUserTable and vacmSecurityToGroupTable
SMIv2 MIB module generation, with NOTIFICATION-TYPE and NOTIFICATION-GROUP 2578, 2579, 2580 Yes (build option ${PARENT}_SNMPSERVER_WITH_MIB_EMITTER, ON by default)
Transport: UDP over IPv4 and IPv6 3417 Yes, one socket per address family
Transport: SSH, TLS, DTLS 5592, 6353 No
AgentX subagent 2741 No
Proxy forwarding 3413 No
FIPS 140-3 operation - Yes (build option ${PARENT}_SNMPSERVER_WITH_FIPS, OFF by default), see FIPS operation

SnmpServer class description

SnmpServer class declaration

The SnmpServer class is declared in the SnmpServer.h file. One object is one agent: one set of listening sockets, one MIB and one worker thread. Class declaration:

namespace rapidpixel
{

/// SNMP agent: command responder and notification originator.
class SnmpServer
{
public:

    /// Reads the value of a scalar object.
    using ReadHandler = std::function<bool(SnmpValue& value)>;

    /// Applies a new value to a scalar object.
    using WriteHandler = std::function<SnmpWriteResult(const SnmpValue& value)>;

    /// Index values of one table row, in declaration order.
    using RowKey = std::vector<SnmpValue>;

    /// Reports the rows of a table.
    using RowsHandler = std::function<bool(std::vector<RowKey>& rows)>;

    /// Reads one cell of a table.
    using RowReadHandler = std::function<bool(const RowKey& row,
                                              SnmpValue& value)>;

    /// Writes one cell of a table.
    using RowWriteHandler = std::function<
        SnmpWriteResult(const RowKey& row, const SnmpValue& value)>;

    /// Handlers of one scalar object.
    struct ObjectHandlers
    {
        /// Reads the value, or empty to serve the value the library holds.
        ReadHandler read{};
        /// Applies a write, or empty to store the value in the library.
        WriteHandler write{};
    };

    /// Handlers of one column of a table.
    struct ColumnHandlers
    {
        /// Column sub-identifier.
        std::uint32_t column{0};
        /// Reads a cell. Required.
        RowReadHandler read{};
        /// Writes a cell. Only for a ReadWrite column.
        RowWriteHandler write{};
    };

    /// Handlers of one table.
    struct TableHandlers
    {
        /// Reports the rows. Required.
        RowsHandler rows{};
        /// Exactly one entry per column of the table, in any order.
        std::vector<ColumnHandlers> columns{};
    };

    /// Get the library version.
    static std::string getVersion();

    /// Check whether the library is built with SNMPv3.
    static bool hasSnmpV3();

    /// Create an idle agent.
    SnmpServer();

    /// Stop the agent and release its resources.
    ~SnmpServer();

    /// Copying is not allowed.
    SnmpServer(const SnmpServer&) = delete;
    SnmpServer& operator=(const SnmpServer&) = delete;

    /// Validate and apply a configuration.
    bool init(const AgentConfig& config);

    /// Get every fault the last init() found.
    std::vector<ConfigFault> getConfigFaults() const;

    /// Get the state of the agent.
    AgentState getState() const;

    /// Get the reason of the last failed call.
    std::string getLastError() const;

    /// Get the reason of the last handler failure.
    std::string getLastHandlerFault() const;

    /// Register a scalar object whose value the library holds.
    bool registerObject(const ObjectDef& def);

    /// Register a scalar object with handlers.
    bool registerObject(const ObjectDef& def, const ObjectHandlers& handlers);

    /// Register a conceptual table.
    bool registerTable(const TableDef& def, const TableHandlers& handlers);

    /// Declare a notification for the MIB module.
    bool registerNotification(const NotificationDef& def);

    /// Update the value the library holds for an object.
    bool setValue(const std::string& oid, const SnmpValue& value);

    /// Read the value the library holds for an object.
    bool getValue(const std::string& oid, SnmpValue& value) const;

    /// Bind the listeners and start answering.
    bool start();

    /// Stop answering and close the listeners.
    void stop();

    /// Send a notification to every configured target.
    bool sendNotification(
        const std::string& oid,
        const std::vector<std::pair<std::string, SnmpValue>>& objects);

    /// Generate the SMIv2 MIB module of the registered objects.
    bool emitMibModule(std::string& out) const;

    /// Get the statistics counters.
    AgentStatistics getStatistics() const;

    /// Get the port a listener is bound to.
    std::uint16_t getBoundPort(std::size_t listenerIndex) const;

    /// Get the snmpEngineID as hex.
    std::string getEngineIdHex() const;

    /// Get the FIPS module version the build accepts.
    static std::string getFipsModuleVersion();

    /// Get the state of the FIPS module.
    FipsModuleState getFipsState() const;

    /// Get the reason the FIPS module is not in approved operation.
    FipsFailureReason getFipsFailureReason() const;

    /// Run the self-tests of the FIPS module.
    bool runFipsSelfTest();

    /// Get the approved-service indicator of the last SNMPv3 message.
    FipsSessionIndicator getFipsSessionIndicator() const;
};
}

Order of calls. init(…) validates and applies the configuration, objects are registered after it, and start() binds the sockets and begins answering. Registration before init(…) or after start() is refused, so a manager can never walk a partly registered MIB:

rapidpixel::SnmpServer agent;
agent.init(config);                   // Validate; nothing is bound yet.
agent.registerObject(object);         // Build the MIB.
agent.registerTable(table, handlers);
agent.start();                        // Bind and begin answering.
...
agent.stop();

Handlers. Handlers may be called concurrently: the current implementation has one worker thread, but the interface does not guarantee it. Handlers must protect the data they share with other threads and must not block. No library lock is held while a handler runs, so a handler may call the methods of the library. A handler that throws an exception does not stop the agent: a read handler that throws is answered as noSuchInstance, a write handler that throws as commitFailed, and a rows handler that throws as an empty table for that request. A read handler that returns a value of another syntax than the declared one, or a value its syntax cannot carry (for example a NaN Real), is answered as noSuchInstance too. These failures are counted in AgentStatistics::handlerFaults and the last one is described by getLastHandlerFault().

Threads. The setup methods init(…), registerObject(…), registerTable(…), registerNotification(…), getConfigFaults(), emitMibModule(…), getEngineIdHex(), start() and stop() must not be called concurrently with each other. setValue(…), getValue(…), sendNotification(…), getState(), getLastError(), getLastHandlerFault(), getStatistics(), getBoundPort(…) and the FIPS methods may be called from any thread at any time, also from a handler. stop() may also be called from a handler. init(…) called from a handler is refused, and so is start() called from a handler that has stopped the agent.

Memory allocation failure. Every method that returns TRUE or FALSE returns FALSE when a memory allocation fails inside it, with “out of memory” in getLastError(), and the object stays usable: a failed init(…) leaves the state ConfigError, a failed start() leaves BindError, a failed registration registers nothing and a failed setValue(…) stores nothing. The methods that return a string, a list or a structure, the constructor and getVersion() may throw std::bad_alloc.

getVersion method

The getVersion() method returns the library version as “Major.Minor.Patch”. The method is static. Method declaration:

static std::string getVersion();

The method can be used without an SnmpServer object:

std::cout << "SnmpServer version: " << rapidpixel::SnmpServer::getVersion();

Console output:

SnmpServer version: 1.0.0

hasSnmpV3 method

The hasSnmpV3() method reports whether the library is built with SNMPv3: USM, the ciphers and the SNMPv3 part of access control (CMake option ${PARENT}_SNMPSERVER_WITH_V3 or ${PARENT}_SNMPSERVER_WITH_FIPS). The interface is the same in every build, so the application needs no conditional compilation: in a build without SNMPv3 a configuration with SNMPv3 users is rejected by init(…) with a configuration fault. The method is static. Method declaration:

static bool hasSnmpV3();

Returns: TRUE if the library is built with SNMPv3 or FALSE if not.

init method

The init(…) method validates the configuration, applies it and builds the MIB with the standard objects (see SNMP support). No socket is bound and no request is answered until start(). Method declaration:

bool init(const AgentConfig& config);
Parameter Description
config Agent configuration: listeners, communities, SNMPv3 users, access control, notification targets, MIB module identity and system objects. See AgentConfig structure.

The method checks the whole configuration and reports every fault it finds, not only the first one (see getConfigFaults method).

When engineBootsFile is set, init(…) reads the boot counter from the file and, if the agent has an engine identifier, stores the counter in it. A boot counter that cannot be read or written makes init(…) fail with the state ConfigError and a fault for engineBootsFile (see AgentConfig structure).

init(…) can be called again on an agent that is not running to apply a new configuration. The registered objects, tables and notifications are removed and must be registered again. snmpEngineBoots is not incremented and snmpEngineTime is not reset by a repeated init(…), so managers keep their cached values. A repeated init(…) re-initializes the SNMP part of the system as RFC 3418 defines it: sysUpTime.0 restarts at zero, the TimeStamp objects sysORLastChange.0 and sysORUpTime restart with it (RFC 2579), and the TestAndIncr objects snmpSetSerialNo.0, vacmViewSpinLock.0 and usmUserSpinLock.0 are incremented. The counters keep running; a manager detects the discontinuity because sysUpTime goes back.

On a running agent init(…) is refused: the state stays Running and getLastError() says that stop() must be called first. init(…) called from a handler is refused too. After a handler has stopped the agent, init(…) waits until the worker thread ends.

Returns: TRUE if the configuration is accepted or FALSE if not. After FALSE the state is ConfigError and getConfigFaults() lists every fault, except on a running agent, which stays in the Running state.

getConfigFaults method

The getConfigFaults() method returns every fault the last init(…) call found. Each fault names the field, the position in the vector and the entry, and gives the reason as text (see ConfigFault structure). Method declaration:

std::vector<ConfigFault> getConfigFaults() const;

Returns: list of faults; empty if the configuration was accepted.

getState method

The getState() method returns the state of the agent (see AgentState enum). Method declaration:

AgentState getState() const;

Returns: state of the agent.

getLastError method

The getLastError() method returns the reason why the last method that returned FALSE failed. There is one text for the whole agent, not one per thread: when two threads fail, the text describes the last failure, so a thread that needs the reason of its own failure reads it directly after the call. The method may be called from any thread and always returns a complete text. Method declaration:

std::string getLastError() const;

Returns: text of the reason or an empty string.

getLastHandlerFault method

The getLastHandlerFault() method returns the description of the last handler failure: a handler threw an exception, returned a value of another syntax than the one its object declares, or returned a value its syntax cannot carry. The affected instance is answered as noSuchInstance, which a manager cannot tell from an object without a value, so this text and the AgentStatistics::handlerFaults counter are the way to detect such failures. getLastError() describes refused method calls; this method describes requests answered with less than the handler should have given. The method may be called from any thread. Method declaration:

std::string getLastHandlerFault() const;

Returns: text of the last handler failure or an empty string if no handler has failed.

registerObject method

The registerObject(…) method registers one scalar object. It must be called between init(…) and start(). The method has two forms.

The first form registers an object whose value the library holds. The application updates the value with setValue(…) whenever it changes, and requests are answered from the stored value without waiting for the application. This form should be preferred. A ReadWrite object of this form stores the values written by managers, and the application reads them with getValue(…). Until the first setValue(…) the object is answered as noSuchInstance. Method declaration:

bool registerObject(const ObjectDef& def);

The second form registers an object with handlers. The read handler is called for every request and must be fast: a std::function that blocks cannot be interrupted. The write handler is called after the library has checked the syntax of the value; it returns SnmpWriteResult::Ok or the reason of the refusal (see SnmpWriteResult enum). A SET whose response would not fit the response size limit is answered tooBig and the write handler is not called (RFC 3416 4.2.5). Method declaration:

bool registerObject(const ObjectDef& def, const ObjectHandlers& handlers);
Parameter Description
def Object definition: OID, descriptor, syntax, access, description, units and scale. See ObjectDef structure.
handlers Read and write handlers. Either of them may be empty: an empty handler is replaced by the value the library holds.

Returns: TRUE if the object is registered or FALSE if not: the method is called outside the period between init(…) and start(), the definition is malformed, the name is not a valid SMIv2 descriptor, the OID overlaps an existing registration, or the OID is one that the MIB module assigns itself (mibModule.rootOid, mibModule.conformanceOid, the arcs under the conformance OID and the named arcs of mibModule.arcs).

registerTable method

The registerTable(…) method registers one conceptual table: its table object, its entry, its INDEX objects and its columns. It must be called between init(…) and start(). Method declaration:

bool registerTable(const TableDef& def, const TableHandlers& handlers);
Parameter Description
def Table definition: OID, descriptor, description, INDEX objects and columns. See TableDef structure.
handlers Rows handler and exactly one ColumnHandlers entry per column.

Rules for the definition and the handlers:

  • def.oid is the OID of the table object. The entry is <def.oid>.1, a column is <def.oid>.1.<column>, and an instance is <def.oid>.1.<column>.<encoded INDEX values> (RFC 2578 7.7).
  • Columns are numbered from indexes.size() + 1: the sub-identifiers from 1 to indexes.size() belong to the INDEX objects, which the MIB module defines. Column numbers must be non-zero and distinct.
  • Every column needs exactly one ColumnHandlers entry with the same column number, in any order, and the entry needs a read handler. Two entries for one column, or an entry for a column the table does not define, make the registration fail. A write handler must be present exactly when the access of the column is Access::ReadWrite.
  • The rows handler reports the current rows as RowKey values: one SnmpValue per INDEX object, in declaration order. The library encodes them into the instance OIDs. The rows handler is called at most once per request, not once per varbind, and the rows need not be sorted or unique. A rows handler that returns FALSE makes the table empty for that request.
  • A cell read handler returns FALSE when the cell has no value, for example when the row was removed after the rows handler reported it; the cell is then answered as noSuchInstance and a walk continues with the next instance.
  • Managers cannot create or delete rows of a registered table.

Returns: TRUE if the table is registered or FALSE if not: the method is called outside the period between init(…) and start(), the definition is malformed, the definition and the handlers do not agree about the columns (a column without an entry or without a read handler, two entries for one column, an entry for a column the table does not define, or a write handler that does not match the access of the column), a column number lies in the range of the INDEX objects, or the OID overlaps an existing registration. After FALSE nothing is registered and getLastError() gives the reason.

registerNotification method

The registerNotification(…) method declares one notification that the agent may send. The declaration puts a NOTIFICATION-TYPE and its NOTIFICATION-GROUP into the generated MIB module; sendNotification(…) also sends notifications that are not declared. Method declaration:

bool registerNotification(const NotificationDef& def);
Parameter Description
def Notification definition: OID, descriptor, description and the objects it carries. See NotificationDef structure.

Two SMIv2 rules apply to a notification that is written into the MIB module:

  • The OID lies under a 0 arc: <root>.3.0.1, not <root>.3.1. RFC 2578 8.5 maps an SNMPv2 notification to an SNMPv1 trap by the sub-identifier under that 0, and every standard module uses this layout. mibModule.arcs therefore names two arcs:

    config.mibModule.arcs.push_back({std::string(ROOT) + ".3",   "devNotifyPrefix"});
    config.mibModule.arcs.push_back({std::string(ROOT) + ".3.0", "devNotifications"});
    
    rapidpixel::NotificationDef faulted;
    faulted.oid = std::string(ROOT) + ".3.0.1";
    

    A notification with another OID is still sent, but emitMibModule(…) refuses to describe it.

  • objects names accessible objects only. RFC 2578 8.1 does not allow a notification to carry a not-accessible object, such as the INDEX object of a table. The index is not needed: a varbind sent at the instance of a row, for example devChannelName.7, already identifies the row.

Returns: TRUE if the notification is registered or FALSE if not.

setValue method

The setValue(…) method updates the value the library holds for a scalar object registered without a read handler. The method may be called from any thread and from a handler. Method declaration:

bool setValue(const std::string& oid, const SnmpValue& value);
Parameter Description
oid OID of the object as it was registered, in dotted decimal notation. For a scalar object, without the trailing .0.
value New value. Its type must be the declared syntax of the object; a value of another type is refused, not converted.

Returns: TRUE if the value is stored or FALSE if not: the OID is not registered, the object is registered with a read handler, the type differs from the declared syntax, or the value cannot be encoded: an OCTET STRING longer than 512 octets, a Real that is not finite or does not fit Integer32 after scaling, an IpAddress that is not four octets, an OBJECT IDENTIFIER with fewer than two or more than 128 sub-identifiers or that cannot be parsed, or an integer outside the range of its syntax. After FALSE the previous value is kept and getLastError() gives the reason.

getValue method

The getValue(…) method reads the value the library holds for an object, for example a value written by a manager to a ReadWrite object registered without a write handler. Method declaration:

bool getValue(const std::string& oid, SnmpValue& value) const;
Parameter Description
oid OID of the object as it was registered, in dotted decimal notation.
value Output value.

Returns: TRUE if the object is registered and the library holds a value for it or FALSE if not.

start method

The start() method binds every listener and starts the worker thread. After it returns TRUE the agent answers requests and registration is refused. Method declaration:

bool start();

If a listener cannot be bound or the worker thread cannot be created, the state is BindError. This state is not terminal: a port can be held for a short time by a previous instance, so start() may be called again.

On a running agent start() returns TRUE at once. After a handler has stopped the agent, start() waits until the worker thread ends and then starts the agent again. start() called from a handler that has stopped the agent is refused, because the worker thread cannot wait for itself.

If notification targets are configured, the agent sends a coldStart notification. A failure to send it does not make start() fail.

The agent sends authenticationFailure (RFC 3418) when a datagram fails authentication (an unknown SNMPv2c community or an SNMPv3 digest that does not verify) and snmpEnableAuthenTraps.0 is enabled(1). The object is disabled(2) until a manager sets it, and at most one authenticationFailure is sent per second, because a sender can trigger it with every datagram. Every refused datagram is still counted in AgentStatistics (inBadCommunityNames for SNMPv2c, usmWrongDigests and the other usm... counters for SNMPv3). An unknown SNMPv3 user or engine identifier causes no notification, because it is also part of normal discovery; the manager receives a Report.

Returns: TRUE if every listener is bound and the worker thread is running or FALSE if not.

stop method

The stop() method stops answering, waits for the worker thread to end and closes every listener. The state becomes Configured; registration and start() are possible again. The method may be called when the agent is not running. Called from a handler, it only signals the worker thread to stop and returns; the worker thread ends after the request it serves, and the next init(…), start() or stop() that the application calls waits until it has ended. The destructor calls this method. Method declaration:

void stop();

sendNotification method

The sendNotification(…) method sends one notification to every configured target: a trap, or an InformRequest to a target with confirmed set (see Confirmed notifications). The method may be called from any thread. Method declaration:

bool sendNotification(
    const std::string& oid,
    const std::vector<std::pair<std::string, SnmpValue>>& objects);
Parameter Description
oid OID of the notification in dotted decimal notation. Usually an OID declared with registerNotification(…); an OID that is not declared is sent as it is, so a standard notification such as linkDown needs no declaration.
objects Objects the notification carries, as (OID, value) pairs. The library puts sysUpTime.0 and snmpTrapOID.0 in front of them, as RFC 3416 4.2.6 requires.

When an OID in objects names a registered object, the value must have the declared type of that object and is encoded with its declared scale. A registered object is named by the instance of a scalar object <oid>.0, by the OID of a scalar object as it was registered (without .0), or by a cell of a registered table <tableOid>.1.<column>.<row>. A value for any other OID is encoded by its own type; a Real value is then sent without scaling, rounded to an integer.

A target whose notify view excludes the notification does not receive it (see Access control). Each target is sent to through the first listener of its address family in AgentConfig::listeners: an IPv6 receiver needs an IPv6 listener, such as "::", and without one every send to that receiver fails. Every datagram has its own request-id; the request-ids start at a random value.

An inform needs a place in the retransmission queue: at most 64 informs wait for an acknowledgment, in one queue for all targets (see Confirmed notifications). When the queue is full, the inform is not sent to that target and is counted in informsAbandoned. If no other target received the notification, the method returns FALSE and getLastError() gives the reason (too many unacknowledged informs); nothing has left the device, so the notification can be sent again when the queue has room. If another target received it (for example a target without confirmed), the method returns TRUE.

Returns: TRUE if a datagram was sent to at least one target or FALSE if nothing was sent: an OID or a value cannot be encoded (for example a value whose type differs from the declared type of the object it names), the agent has not been started, no target is configured or reachable, the notify views exclude the notification for every target, or the inform queue is full and no other target received the notification. After FALSE getLastError() gives the reason. When memory runs out after the notification was sent to some targets, the remaining targets are not tried and the method returns TRUE.

emitMibModule method

The emitMibModule(…) method generates the SMIv2 MIB module that describes the registered objects, tables and notifications. The module and the agent cannot differ, because both are built from the same registrations. Registrations outside mibModule.rootOid are not included: the standard objects are described by their own standard modules. Method declaration:

bool emitMibModule(std::string& out) const;
Parameter Description
out Output text of the module; empty on failure.

The method refuses to write an incomplete module. Generation stops with an error for an object without a descriptor or a description, a descriptor that is not an SMIv2 identifier, two objects with the same name, a table whose INDEX definition does not match, an object whose parent arc has no name in mibModule.arcs, a notification outside a 0 arc, a notification that carries an object the module does not define, a text that contains a double quote, and a syntaxOverride that is blank or contains a control character, a double quote, the comment marker -- or unbalanced parentheses or braces.

The generator can be left out of the build with ${PARENT}_SNMPSERVER_WITH_MIB_EMITTER OFF, which makes the library smaller and does not change what the agent serves. The method then clears out, returns FALSE and getLastError() names the option.

Returns: TRUE if the module is generated or FALSE if not. After FALSE, getLastError() names the object that could not be described and the reason.

getStatistics method

The getStatistics() method returns the statistics counters of the agent (see AgentStatistics structure). Most of them are also served as MIB objects. The method may be called from any thread. Method declaration:

AgentStatistics getStatistics() const;

Returns: snapshot of the counters.

AgentStatistics contains four of the six usmStats counters. The agent serves all six as MIB objects; the other two can be read only as MIB objects:

Table 3 - usmStats counters that are not in AgentStatistics.

Counter OID Meaning of a rising value
usmStatsUnsupportedSecLevels 1.3.6.1.6.3.15.1.1.1.0 A manager requested a security level its user is not configured for.
usmStatsUnknownEngineIDs 1.3.6.1.6.3.15.1.1.4.0 Normal traffic: every SNMPv3 manager starts with a discovery exchange that increments it once.

getBoundPort method

The getBoundPort(…) method returns the port a listener is bound to. It is useful for a configuration with port 0, for which the operating system selects a free port. Method declaration:

std::uint16_t getBoundPort(std::size_t listenerIndex) const;
Parameter Description
listenerIndex Index of the listener in AgentConfig::listeners.

Returns: bound port or 0 if the listener is not bound.

getEngineIdHex method

The getEngineIdHex() method returns the snmpEngineID of the agent as lowercase hex digits. The identifier is built by init(…) from the enterprise number in mibModule.rootOid and engineIdSerial (RFC 3411 5), in every build, also without SNMPv3. Managers cache the identifier, so it should be printed at start-up: a change across restarts forces every manager to discover the engine again. Method declaration:

std::string getEngineIdHex() const;

Returns: engine identifier as hex, or an empty string if engineIdSerial or mibModule.rootOid is not set.

getFipsModuleVersion method

The getFipsModuleVersion() method returns the version of the FIPS module that the build accepts. A FIPS module is validated in a specific version, so the build accepts only the version set with the CMake option ${PARENT}_SNMPSERVER_FIPS_MODULE_VERSION (default 3.1.2), and refuses any other. While getFipsState() returns Operational, this is also the version in use. The method is static. Method declaration:

static std::string getFipsModuleVersion();

Returns: version, for example “3.1.2”, or an empty string in a build without ${PARENT}_SNMPSERVER_WITH_FIPS.

getFipsState method

The getFipsState() method returns the state of the FIPS module used by the agent (the “show status” service of ISO/IEC 19790 7.4.3), see FipsModuleState enum. The state is measured, not declared: the version, the self-test result and the approved-mode settings are read from the module. The method exists in every build and may be called from any thread, before and after init(…). Method declaration:

FipsModuleState getFipsState() const;

Returns: state of the FIPS module.

getFipsFailureReason method

The getFipsFailureReason() method returns the reason why the FIPS module is not in approved operation, see FipsFailureReason enum. getConfigFaults() contains the same reason as text. Method declaration:

FipsFailureReason getFipsFailureReason() const;

Returns: reason; FipsFailureReason::None when the state is Operational.

Table 4 - Pairs of FIPS module state and failure reason.

getFipsState() getFipsFailureReason()
NotBuilt NotBuiltIn
NotRequested NotRequested
NotLoaded, Operational None
Error Any other value.

runFipsSelfTest method

The runFipsSelfTest() method runs the self-tests of the FIPS module on demand (ISO/IEC 19790 7.10.3). A failure is permanent for the life of the process: the module enters its error state, every cryptographic operation is refused, the agent answers no SNMPv3 message and continues to answer SNMPv2c requests. The method may be called from any thread. Method declaration:

bool runFipsSelfTest();

Returns: TRUE if every self-test passed or FALSE if not. The method also returns FALSE when FIPS operation is not active; getFipsState() distinguishes the two cases.

getFipsSessionIndicator method

The getFipsSessionIndicator() method returns the approved-service indicator for the last SNMPv3 request the agent accepted (FIPS 140-3 Implementation Guidance 2.4.C), see FipsSessionIndicator structure. getFipsState() reports whether the module is in approved operation; this method reports which algorithms protected a message. Every SNMPv3 request carries its own user, and users may have different algorithms, so the indicator describes one message, not a connection. The security level is taken from the msgFlags of the message. Every protocol of the library is approved, so in FIPS operation the indicator is approved for every authenticated message; a noAuthNoPriv message is never approved. Outside FIPS operation approved is always FALSE, and the other fields show which algorithms the managers use. The method exists in every build and may be called from any thread. Method declaration:

FipsSessionIndicator getFipsSessionIndicator() const;

Returns: security parameters of the last accepted SNMPv3 request; measured is FALSE until the first such request.

Data model

The data structures are declared in the SnmpServerTypes.h file, in the rapidpixel namespace. Every field has a default value, and init(…) checks the configuration field by field (see ConfigFault structure). Conventions:

  • OIDs are passed as dotted-decimal strings without a leading dot, for example "1.3.6.1.4.1.99999.1.2". The OID of a scalar object is given without the trailing .0; the library appends it.
  • Units are text (the SMIv2 UNITS clause); the decimal scale of a real value is a typed exponent (SnmpScale).
  • The library predefines no enterprise OID: mibModule.rootOid is set by the application under its own IANA enterprise number.
  • Network addresses are numeric. The library resolves no host names.

Constants

Table 5 - Constants.

Constant Type Value Description
SNMP_AGENT_PORT std::uint16_t 161 UDP port of an SNMP agent (RFC 3417 4); default of ListenerConfig::port. Binding it needs privileges on most systems.
SNMP_TRAP_PORT std::uint16_t 162 UDP port notifications are sent to (RFC 3417 4); default of TrapTargetConfig::port.
DEFAULT_INFORM_TIMEOUT_MS std::uint32_t 5000 Time an inform waits for its acknowledgment before it is sent again, milliseconds; default of TrapTargetConfig::timeoutMs.
SYS_SERVICES_HOST std::uint8_t 72 sysServices of a host that offers application services: end-to-end layer (8) and applications layer (64), RFC 3418; default of AgentConfig::sysServices.

AgentConfig structure

The AgentConfig structure contains the whole configuration of the agent, see init method.

Table 6 - AgentConfig fields.

Field Type Default Description
listeners std::vector<ListenerConfig> empty Sockets to bind. Empty is a configuration fault. Notifications to a target leave through the first listener of the address family of the target.
communities std::vector<CommunityConfig> empty SNMPv2c communities the agent accepts. Empty accepts none (an SNMPv3-only agent).
usmUsers std::vector<UsmUserConfig> empty SNMPv3 users. Must be empty in a build without SNMPv3.
vacm VacmConfig access control disabled Access control, see Access control.
trapTargets std::vector<TrapTargetConfig> empty Notification targets. Empty sends no notifications. The list is accepted or refused as a whole: an entry with a fault makes init(…) fail.
mibModule MibModuleConfig all fields empty Identity of the generated MIB module. rootOid also provides the enterprise number of the engine identifier.
sysDescr std::string empty Value of sysDescr.0: a description of the system.
sysContact std::string empty Value of sysContact.0.
sysName std::string empty Value of sysName.0: the administratively assigned name.
sysLocation std::string empty Value of sysLocation.0.
sysServices std::uint8_t SYS_SERVICES_HOST (72) Value of sysServices.0, from 0 to 127 (RFC 3418).
sysObjectId std::string empty Value of sysObjectID.0: the OID that identifies the system type. Empty serves mibModule.rootOid.
engineIdSerial std::string empty Serial part of snmpEngineID, combined with the enterprise number from mibModule.rootOid (which must lie under 1.3.6.1.4.1). It must be stable across restarts and unique per system: a changed identifier forces every manager to discover the engine again. Required for every SNMPv3 user, noAuthNoPriv included (an agent without an engine identifier serves no SNMPv3, and the keys of a user are localized to the engine identifier), and for serveCommunityTable.
engineBootsFile std::string empty Path of the file snmpEngineBoots is stored in, on storage that survives a restart. Required when an SNMPv3 user authenticates: without it every restart starts the counter at 1 again, and an authenticated request recorded before the restart would be accepted after it (RFC 3414 2.2.1). Empty is allowed for an SNMPv2c-only agent and for users without authentication; the counter is then kept in memory. A file that does not exist is a first boot. A file that exists but cannot be read makes init(…) fail, and so does a file whose last line lacks the line end that the library writes (a file cut short while being written). A file larger than 4096 octets is refused. A counter that cannot be written (a folder that does not exist or is not writable, a full device) makes init(…) fail with the state ConfigError and a fault for engineBootsFile. Without an engine identifier (engineIdSerial or mibModule.rootOid empty) the file is not written. The file is written through a temporary file <path>.new, created exclusively, readable by the owner only and never through a symbolic link; the directory must exist and should be writable by the agent only.
serveCommunityTable bool false Serve snmpCommunityTable (RFC 3584), see snmpCommunityTable.
maxResponseOctets std::uint32_t 0 Largest response the agent sends, octets; 0 means the transport maximum. This is the local constraint of RFC 3416 4.2.5: a GETBULK returns fewer rows, a GET that does not fit is answered tooBig, and a SET that does not fit is answered tooBig and not applied. Values from 1 to 483 are refused: 484 is the smallest message size every SNMP implementation must accept (RFC 3412 6.3).
fipsEnabled bool false Take every cryptographic operation from the validated FIPS module and answer no SNMPv3 message if that is not possible, see FIPS operation. Ignored in a build without ${PARENT}_SNMPSERVER_WITH_FIPS.
fipsModuleDir std::string empty Folder of the FIPS module (fips.so, on Windows fips.dll). Empty uses the default module folder of OpenSSL.
fipsConfigFile std::string empty OpenSSL configuration file that includes fipsmodule.cnf, the installation data the module checks itself against. Empty uses the default openssl.cnf.

ListenerConfig structure

The ListenerConfig structure describes one listening socket. One socket is used per address family, which keeps the bound addresses explicit in the configuration.

Table 7 - ListenerConfig fields.

Field Type Default Description
address std::string "0.0.0.0" Address to bind: "0.0.0.0" for every IPv4 interface, "::" for every IPv6 interface, or one address. Numeric only; a host name is a configuration fault. Empty binds the IPv4 wildcard.
port std::uint16_t SNMP_AGENT_PORT (161) UDP port. 0 lets the operating system select a free port (see getBoundPort method).

CommunityConfig structure

The CommunityConfig structure describes one SNMPv2c community. A community is sent in clear text over the network and authenticates nobody. It is mapped to a VACM security name, so the views of a community are decided by the same access control as for SNMPv3 users (RFC 3584).

Table 8 - CommunityConfig fields.

Field Type Default Description
community std::string empty Community string, 1 to 64 octets.
securityName std::string empty VACM security name the community maps to. Empty means the community itself; with access control enabled a security name is limited to 32 octets, so a longer community needs an explicit security name.

UsmUserConfig structure

The UsmUserConfig structure describes one SNMPv3 user. Each key (authentication and privacy) has three possible sources: a passphrase file, a file with the localized key in hex, or an inline passphrase. Files are the preferred form; an inline passphrase is held in a structure that can be copied and logged by mistake. A user with authentication needs exactly one authentication source, and at most one privacy source may be given: two sources for one key are a configuration fault, not a precedence rule. A user without authentication may have no credential and no privacy protocol. A credential file is read up to its first line break, and spaces and tabs at the end of the line are removed; the first line may hold at most 1024 octets, and a longer line is a fault naming the file. A passphrase must have at least 8 octets (RFC 3414 11.2): the authentication passphrase, and the privacy passphrase too, which has a fault of its own. A localized authentication key (authKeyFile) belongs to the engine identifier of the agent, so it needs engineIdSerial and mibModule.rootOid, and with a privacy protocol it requires a localized privacy key (privKeyFile). privKeyFile is used only together with authKeyFile: with an authentication passphrase and a privacy protocol it is a configuration fault. Every SNMPv3 user, noAuthNoPriv included, needs engineIdSerial and mibModule.rootOid; otherwise init(…) reports a fault for the user.

Table 9 - UsmUserConfig fields.

Field Type Default Description
name std::string empty User name, 1 to 32 octets (msgUserName).
auth AuthProtocol AuthProtocol::None Authentication protocol.
priv PrivProtocol PrivProtocol::None Privacy protocol. Requires an authentication protocol.
authPassphraseFile std::string empty Path of a file with the authentication passphrase in its first line.
authKeyFile std::string empty Path of a file with the localized authentication key as hex. Requires engineIdSerial and mibModule.rootOid.
authPassphrase std::string empty Authentication passphrase inline. Not recommended.
privPassphraseFile std::string empty Path of a file with the privacy passphrase, at least 8 octets.
privKeyFile std::string empty Path of a file with the localized privacy key as hex. Used only together with authKeyFile.
privPassphrase std::string empty Privacy passphrase inline, at least 8 octets. Not recommended. If no privacy source is given and a privacy protocol is set, the authentication passphrase is used.
securityName std::string empty Security name of the user for access control (usmUserSecurityName), at most 32 octets. Empty means the user name. When set, it replaces the user name in access control, so a GroupConfig must name this value. It does not change usmUserTable: the rows and the key change of a user’s own key (usmUserOwnAuthKeyChange) use the user name.

AuthProtocol enum

The AuthProtocol enum (underlying type std::uint8_t) defines the authentication protocol of a USM user. Only the SHA-2 protocols of RFC 7860 are supported; the values 1 and 2 (HMAC-MD5-96 and HMAC-SHA-96 of RFC 3414) are not used, and init(…) refuses any value not listed.

Table 10 - AuthProtocol values.

Value Number Description
None 0 No authentication (noAuthNoPriv).
HmacSha224 3 usmHMAC128SHA224AuthProtocol (RFC 7860). Not available with the Windows CNG backend of CryptoProvider.
HmacSha256 4 usmHMAC192SHA256AuthProtocol (RFC 7860).
HmacSha384 5 usmHMAC256SHA384AuthProtocol (RFC 7860).
HmacSha512 6 usmHMAC384SHA512AuthProtocol (RFC 7860).

PrivProtocol enum

The PrivProtocol enum (underlying type std::uint8_t) defines the privacy protocol of a USM user. Only AES is supported; the value 1 (DES-CBC of RFC 3414) is not used, and init(…) refuses any value not listed.

Table 11 - PrivProtocol values.

Value Number Description
None 0 No encryption.
Aes128 2 usmAesCfb128Protocol (RFC 3826).
Aes192 3 AES-192 in CFB mode. Not standardized for USM (Internet-Draft); requires HMAC-SHA-224 or a stronger authentication protocol (see Limitations).
Aes256 4 AES-256 in CFB mode. Not standardized for USM (Internet-Draft); requires HMAC-SHA-256 or a stronger authentication protocol.

VacmConfig structure

The VacmConfig structure contains the access control configuration, see Access control.

Table 12 - VacmConfig fields.

Field Type Default Description
enabled bool false TRUE to enforce VACM. FALSE: every principal the security model accepts reads every object, and only an authenticated SNMPv3 user writes.
views std::vector<ViewConfig> empty View families.
access std::vector<AccessConfig> empty Access grants of the groups.
groups std::vector<GroupConfig> empty Group memberships.

ViewConfig structure

The ViewConfig structure describes one family of subtrees of a MIB view (RFC 3415). All entries with the same name form one view.

Table 13 - ViewConfig fields.

Field Type Default Description
name std::string empty View name, 1 to 32 octets.
subtree std::string empty Root of the family, dotted decimal.
included bool true TRUE to include the family, FALSE to exclude it. An excluded family under an included one removes that branch from the view.
maskHex std::string empty Family mask as hex octets, at most 16 octets, for example "ffa0". One bit per sub-identifier, from the most significant bit of the first octet: 1 means the sub-identifier must match, 0 means any value matches. A mask shorter than the subtree is extended with ones, so an empty mask means every sub-identifier must match.

AccessConfig structure

The AccessConfig structure describes the access grant of one group.

Table 14 - AccessConfig fields.

Field Type Default Description
groupName std::string empty Group name.
level SecurityLevel SecurityLevel::NoAuthNoPriv Minimum security level of a request for this grant. A grant applies at its own level and above.
readView std::string empty View for GET, GETNEXT and GETBULK. Empty grants no read access.
writeView std::string empty View for SET. Empty grants no write access.
notifyView std::string empty View a notification must lie in before it is sent to a target whose community maps to this group (RFC 3413 3.3). Empty filters nothing (see Access control).

SecurityLevel enum

The SecurityLevel enum (underlying type std::uint8_t) defines the security level of a request or a grant.

Table 15 - SecurityLevel values.

Value Description
NoAuthNoPriv Neither authenticated nor encrypted.
AuthNoPriv Authenticated, not encrypted.
AuthPriv Authenticated and encrypted.

GroupConfig structure

The GroupConfig structure describes the group membership of one principal.

Table 16 - GroupConfig fields.

Field Type Default Description
securityName std::string empty Security name: of a USM user, or the one a community maps to.
groupName std::string empty Group name.
community bool false TRUE if securityName belongs to an SNMPv2c community, FALSE if it belongs to a USM user. The security model is part of the membership, so a community and a user with the same name can have different rights.

TrapTargetConfig structure

The TrapTargetConfig structure describes one notification target. Notifications are sent as SNMPv2c traps or informs.

Table 17 - TrapTargetConfig fields.

Field Type Default Description
address std::string empty Numeric IPv4 or IPv6 address of the receiver: 192.0.2.10, 2001:db8::5, or a link-local address with its zone, fe80::1%eth0 (RFC 4007 11). Required. A host name is a configuration fault. An IPv6 zone must be a non-zero interface index or the name of an interface present at init(…); an empty or unknown zone is a configuration fault. Notifications to the receiver leave through the first listener of its address family, so an IPv6 receiver needs an IPv6 listener such as "::"; without one every send to it fails and getLastError() names the address.
port std::uint16_t SNMP_TRAP_PORT (162) UDP port of the receiver. 0 is a configuration fault.
community std::string empty Community of the notification. Required; it has no default.
confirmed bool false TRUE to send an InformRequest instead of a trap (RFC 3416 4.2.7), see Confirmed notifications.
timeoutMs std::uint32_t DEFAULT_INFORM_TIMEOUT_MS (5000) Time to wait for the acknowledgment before retransmission, milliseconds. Only for a confirmed target.
retries std::uint32_t 3 Number of retransmissions after the first attempt (0 sends once), at most 255 (snmpTargetAddrRetryCount, RFC 3413 4.1.1). Only for a confirmed target. timeoutMs * (retries + 1) may not exceed one hour.

MibModuleConfig structure

The MibModuleConfig structure describes the MODULE-IDENTITY of the generated MIB module (RFC 2578 5). The library defaults none of the fields: the module name, the OIDs and the organization belong to the application.

Table 18 - MibModuleConfig fields.

Field Type Default Description
moduleName std::string empty Module name, upper case with hyphens, conventionally ending in -MIB, for example DEVICE-MIB.
identityName std::string empty Descriptor of the MODULE-IDENTITY, for example deviceMIB.
rootOid std::string empty Enterprise root all objects are registered under, dotted decimal: 1.3.6.1.4.1.<IANA enterprise number>.
conformanceOid std::string empty OID of the conformance information, dotted decimal.
lastUpdated std::string empty LAST-UPDATED in the SMIv2 form YYYYMMDDHHMMZ.
organization std::string empty ORGANIZATION clause.
contactInfo std::string empty CONTACT-INFO clause.
description std::string empty DESCRIPTION clause of the module.
arcs std::vector<std::pair<std::string, std::string>> empty Named arcs between the root and the objects, as (dotted OID, name). SMIv2 assigns every object under a named parent, so every arc between the root and an object needs a name.

ConfigFault structure

The ConfigFault structure describes one fault of a configuration, see getConfigFaults method.

Table 19 - ConfigFault fields.

Field Type Default Description
field std::string empty Name of the field as in AgentConfig, for example listeners, usmUsers or vacm.views.
index std::size_t 0 Position in the vector, or 0 for a field that is not a vector.
entryName std::string empty Name of the entry: a user name, a view name, a group name, a security name, a listener or target address. Empty if the entry has no name, and for an entry of communities.
reason std::string empty Reason of the fault as text.

AgentState enum

The AgentState enum (underlying type std::uint8_t) defines the state of the agent, see getState method.

Table 20 - AgentState values.

Value Description
Idle Created, no configuration yet. No socket and no worker thread.
Configured init(…) accepted a configuration, or stop() stopped the agent. Registration and start() are possible.
Running start() bound the listeners and the agent answers requests.
BindError A listener could not be bound or the worker thread could not be created. Not terminal: start() may be called again, and getLastError() gives the reason.
ConfigError init(…) rejected the configuration or the boot counter could not be stored, see getConfigFaults().
CryptoError The cryptographic provider failed. Terminal: only init(…) clears it.

SnmpType enum

The SnmpType enum (underlying type std::uint8_t) defines the syntax of an object (RFC 2578 7.1). A textual convention such as DisplayString is one of these types; its name is given in ObjectDef::syntaxOverride so that the MIB module names it.

Table 21 - SnmpType values.

Value Description
Integer Integer32, signed (RFC 2578 7.1.1).
OctetString OCTET STRING: text or binary data.
ObjectIdentifier OBJECT IDENTIFIER, dotted decimal (RFC 2578 7.1.3).
IpAddress IpAddress, exactly four octets (RFC 2578 7.1.5).
Counter32 Counter32: unsigned, increasing, wraps at 2^32 (RFC 2578 7.1.6).
Gauge32 Gauge32 or Unsigned32: unsigned, latches at its bounds (RFC 2578 7.1.7).
TimeTicks TimeTicks, hundredths of a second (RFC 2578 7.1.8).
Counter64 Counter64, unsigned 64-bit (RFC 2578 7.1.10).
Real Not an SNMP type: a real value sent as an Integer32 scaled with ObjectDef::scale. SNMP has no floating-point syntax.

SnmpScale enum

The SnmpScale enum (underlying type std::int8_t) defines the decimal exponent of a SnmpType::Real value. The library sends llround(value * 10^scale): with Deci, a value of 21.5 is sent as 215, and the UNITS clause of the MIB module, for example “tenths of a degree Celsius”, tells a manager how to read it.

Table 22 - SnmpScale values.

Value Number Description
Unit 0 No scaling: 21.5 is sent as 22 (rounded).
Deci 1 One decimal place: 21.5 is sent as 215.
Centi 2 Two decimal places: 21.55 is sent as 2155.
Milli 3 Three decimal places: 21.555 is sent as 21555.
Micro 6 Six decimal places.

SnmpValue structure

The SnmpValue structure holds one value in either direction. type defines which field is used; a handler fills only the field of the declared syntax. In a read handler the type is already set to the declared syntax of the object.

Table 23 - SnmpValue fields.

Field Type Default Description
type SnmpType SnmpType::Integer Which field below is used.
integer std::int64_t 0 Value of Integer, Counter32, Gauge32, TimeTicks and Counter64.
real double 0.0 Value of Real, scaled with the scale of the object.
octets std::string empty Value of OctetString, and the four octets of an IpAddress.
oid std::string empty Value of ObjectIdentifier, dotted decimal without a leading dot.

The structure has static functions that create a value of each type:

Table 24 - SnmpValue functions.

Function Description
static SnmpValue makeInteger(std::int32_t value) Integer32 value.
static SnmpValue makeCounter32(std::uint32_t value) Counter32 value.
static SnmpValue makeGauge32(std::uint32_t value) Gauge32 or Unsigned32 value.
static SnmpValue makeTimeTicks(std::uint32_t hundredths) TimeTicks value, hundredths of a second.
static SnmpValue makeCounter64(std::uint64_t value) Counter64 value.
static SnmpValue makeIpAddress(const std::string& octets) IpAddress value from exactly four octets, most significant first. For an IpAddress object use this function, not makeString(): a value of type OctetString does not match the IpAddress syntax.
static SnmpValue makeString(const std::string& value) OCTET STRING value, at most 512 octets.
static SnmpValue makeOid(const std::string& dotted) OBJECT IDENTIFIER value from dotted decimal without a leading dot.
static SnmpValue makeReal(double value) Real value: a finite number that fits Integer32 after scaling.

SnmpWriteResult enum

The SnmpWriteResult enum (underlying type std::uint8_t) defines the result of a write handler. The library sends the corresponding SNMP error status (RFC 3416 4.2.5), so a manager can tell the reasons apart.

Table 25 - SnmpWriteResult values.

Value Error status Description
Ok noError The value was applied.
WrongValue wrongValue The value is valid for the syntax but never valid for this object.
WrongLength wrongLength The value is outside the SIZE constraint of the object.
InconsistentValue inconsistentValue The value is valid for the object but not in its present state.
ResourceUnavailable resourceUnavailable A resource the write needs is not available.
Failed commitFailed The value passed validation but could not be applied.

Access enum

The Access enum (underlying type std::uint8_t) defines the MAX-ACCESS of an object. SMIv2 has no write-only access (RFC 2578 7.3).

Table 26 - Access values.

Value Description
ReadOnly A manager may read the object. MAX-ACCESS read-only.
ReadWrite A manager may read and write the object. MAX-ACCESS read-write.

ObjectDef structure

The ObjectDef structure describes one scalar object or one column of a table. name and description are required by emitMibModule(…).

Table 27 - ObjectDef fields.

Field Type Default Description
oid std::string empty OID in dotted decimal notation without a leading dot. For a scalar object, the object OID without the trailing .0. Not used for a column (see column).
name std::string empty SMIv2 descriptor, for example devChannelCount: 1 to 64 characters, starting with a lowercase letter, letters and digits only (RFC 2578 3.1).
type SnmpType SnmpType::Integer Syntax of the object.
access Access Access::ReadOnly Access of the object; it is enforced by the agent and written into the MIB module. For a scalar object, ReadWrite without a write handler stores written values in the library (read them with getValue(…)). For a column, ReadWrite must be combined with a write handler and ReadOnly with none.
description std::string empty Text of the DESCRIPTION clause.
units std::string empty Text of the UNITS clause (RFC 2578 7.9), for example the unit of a scaled value.
scale SnmpScale SnmpScale::Unit Decimal exponent of a Real object; ignored for other syntaxes.
syntaxOverride std::string empty Explicit SYNTAX clause for the MIB module, for example DisplayString (SIZE(0..64)) or INTEGER { up(1), down(2) }. Empty writes the base type. The text is written into the module as it is, so emitMibModule(…) refuses a blank text, a control character, a double quote, the comment marker -- and unbalanced parentheses or braces.
syntaxImports std::vector<std::pair<std::string, std::string>> empty Imports syntaxOverride needs, as (name, module), for example {"DisplayString", "SNMPv2-TC"}. Base types are included: Integer32 (0..255) needs {"Integer32", "SNMPv2-SMI"}. OCTET STRING and INTEGER { ... } need no import.
column std::uint32_t 0 Column sub-identifier of a table column, from indexes.size() + 1; ignored for a scalar object. 0 is not valid for a column.

IndexType enum

The IndexType enum (underlying type std::uint8_t) defines how an INDEX value is encoded into an instance OID (RFC 2578 7.7).

Table 28 - IndexType values.

Value Description
Integer One sub-identifier with the value. The MIB module declares the index Integer32 (0..2147483647) and row keys outside that range are refused. For a larger range, set IndexDef::syntaxOverride to Unsigned32 (0..4294967295) with its import.
String A length sub-identifier, then one sub-identifier per octet. A variable-length string index needs a syntaxOverride with a SIZE for the MIB module.
FixedString One sub-identifier per octet; the number of octets is IndexDef::length.
ObjectIdentifier A length sub-identifier, then the sub-identifiers of the value.

IndexDef structure

The IndexDef structure describes one INDEX object of a table.

Table 29 - IndexDef fields.

Field Type Default Description
name std::string empty SMIv2 descriptor.
type IndexType IndexType::Integer Encoding of the value in the instance OID.
length std::size_t 0 Number of octets for IndexType::FixedString; ignored otherwise.
description std::string empty Text of the DESCRIPTION clause.
syntaxOverride std::string empty Explicit SYNTAX clause, or empty to derive one. Checked like ObjectDef::syntaxOverride.
syntaxImports std::vector<std::pair<std::string, std::string>> empty Imports syntaxOverride needs, as (name, module), like ObjectDef::syntaxImports.

TableDef structure

The TableDef structure describes one conceptual table, see registerTable method.

Table 30 - TableDef fields.

Field Type Default Description
oid std::string empty OID of the table object (not of the entry), dotted decimal.
name std::string empty Descriptor, conventionally ending in Table. The entry descriptor is derived from it: devChannelTable gets devChannelEntry.
description std::string empty Text of the DESCRIPTION clause.
indexes std::vector<IndexDef> empty INDEX objects in the order of the INDEX clause. At least one is required; the order is part of the encoding.
columns std::vector<ObjectDef> empty Columns. At least one is required; column numbers must be non-zero and distinct.

NotificationDef structure

The NotificationDef structure describes one notification, see registerNotification method.

Table 31 - NotificationDef fields.

Field Type Default Description
oid std::string empty OID of the NOTIFICATION-TYPE, dotted decimal. To appear in the MIB module it must lie under a 0 arc.
name std::string empty SMIv2 descriptor, for example devChannelFaulted.
description std::string empty Text of the DESCRIPTION clause.
objects std::vector<std::string> empty OBJECTS clause: descriptors of the objects the notification carries, in order. Each must be an accessible object registered under mibModule.rootOid.

AgentStatistics structure

The AgentStatistics structure contains the counters of the agent, see getStatistics method. Every field has the type std::uint64_t and the default 0.

Table 32 - AgentStatistics fields.

Field Description
inPackets Datagrams received.
outPackets Datagrams the transport accepted from the agent: responses, Reports and notifications. Not an RFC 3418 object.
inAsn1ParseErrors Datagrams that could not be decoded.
inBadCommunityNames Requests refused for an unknown community.
inBadCommunityUses Requests a community was not authorized for.
inBadVersions Requests with an SNMP version the agent does not serve.
silentDrops snmpSilentDrops of RFC 3418: confirmed-class requests dropped because not even an empty Response would fit.
usmWrongDigests SNMPv3 requests refused because the digest did not verify.
usmUnknownUserNames SNMPv3 requests refused for an unknown user name.
usmNotInTimeWindows SNMPv3 requests refused because their time was outside the time window.
usmDecryptionErrors SNMPv3 requests refused because the scoped PDU could not be decrypted.
notificationsSent Notifications sent, counted once per target; retransmissions are not included.
informsAcknowledged InformRequests a receiver acknowledged.
informsPending InformRequests waiting for an acknowledgment at this moment.
informsAbandoned InformRequests that were not acknowledged after the last retransmission, not sent because the queue was full, or dropped while pending when the agent stopped or its targets were replaced. Each one is a notification that no receiver confirmed.
informRetransmits Retransmissions, one per attempt after the first.
notificationsSuppressed Notifications not sent to a target because its notify view excluded them. Zero unless an AccessConfig::notifyView is configured.
internalErrors Datagrams dropped because processing raised an exception in the worker thread, usually std::bad_alloc. Should be zero.
handlerFaults Failures of handlers of the application: an exception, a value of another syntax than the declared one, or a value its syntax cannot carry. See getLastHandlerFault method.

FipsModuleState enum

The FipsModuleState enum (underlying type std::uint8_t) defines the state of the FIPS module, see getFipsState method.

Table 33 - FipsModuleState values.

Value Description
NotBuilt The library is built without ${PARENT}_SNMPSERVER_WITH_FIPS.
NotRequested FIPS-capable build, AgentConfig::fipsEnabled is FALSE.
NotLoaded FIPS operation is requested, the module is not loaded yet: init(…) has not run.
Operational The module is loaded, its self-tests passed, its run-time security checks are on and its version is the accepted one.
Error FIPS operation is requested and the module cannot be used. The agent does not fall back to the standard mode and answers no SNMPv3 message, see FipsFailureReason enum.

FipsFailureReason enum

The FipsFailureReason enum (underlying type std::uint8_t) defines why the FIPS module is not in approved operation, see getFipsFailureReason method.

Table 34 - FipsFailureReason values.

Value Description
None No failure.
NotBuiltIn The library is built without ${PARENT}_SNMPSERVER_WITH_FIPS.
NotRequested AgentConfig::fipsEnabled is FALSE.
ModulePathInvalid AgentConfig::fipsModuleDir is rejected.
ConfigLoadFailed AgentConfig::fipsConfigFile is missing or malformed, or its include of fipsmodule.cnf cannot be resolved.
ModuleLoadFailed The module is missing or cannot be loaded, including a failed integrity check.
SelfTestFailed The self-tests of the module did not pass.
SecurityChecksDisabled The security-checks setting of the module is off.
TruncDigestAllowed The drbg-no-trunc-md setting of the module is off.
ModuleVersionMismatch The module version differs from the accepted one (getFipsModuleVersion()).
ModulePathConflict The module in the process was loaded from other paths.
InternalError Memory allocation or cryptographic library failure.

FipsSessionIndicator structure

The FipsSessionIndicator structure contains the security parameters of the last SNMPv3 request the agent accepted, see getFipsSessionIndicator method. Its fields keep their default values until the first such request.

Table 35 - FipsSessionIndicator fields.

Field Type Default Description
approved bool false TRUE if the FIPS module is Operational, the message is authenticated with an approved protocol and, if it is encrypted, encrypted with an approved protocol. Never TRUE outside FIPS operation.
measured bool false TRUE after the first SNMPv3 request was accepted, so that a default value is not mistaken for noAuthNoPriv.
auth AuthProtocol AuthProtocol::None Authentication protocol the message was verified with.
priv PrivProtocol PrivProtocol::None Privacy protocol the scoped PDU was decrypted with.
level SecurityLevel SecurityLevel::NoAuthNoPriv Security level of the message.
securityName std::string empty User name the message was authenticated with; empty for noAuthNoPriv.

Access control

Access control is configured with AgentConfig::vacm (see VacmConfig structure) and has two modes.

With vacm.enabled FALSE (the default), every principal the security model accepts reads every object: for SNMPv2c, everyone who knows a community. Only an authenticated principal writes, that is an SNMPv3 user whose digest was verified. A SET sent with an SNMPv2c community is answered noAccess, because a community is a clear-text, replayable token that authenticates nobody.

With vacm.enabled TRUE, VACM (RFC 3415) decides every access, also the write access of communities: nothing is accessible until a group membership, an access grant and a view allow it. The example below gives a community read access to everything except the security tables:

// The community is a credential: read it from a file that only the account
// of the agent can read. readFirstLine() is a function of the application.
rapidpixel::CommunityConfig community;
community.community = readFirstLine("/path/to/community");
community.securityName = "publicreader";
config.communities.push_back(community);

config.vacm.enabled = true;

rapidpixel::ViewConfig everything;
everything.name = "everything";
everything.subtree = "1.3.6.1";
config.vacm.views.push_back(everything);

// Exclude the USM and VACM tables from the view of the community.
rapidpixel::ViewConfig hideUsm;
hideUsm.name = "everything";
hideUsm.subtree = "1.3.6.1.6.3.15";
hideUsm.included = false;
config.vacm.views.push_back(hideUsm);

rapidpixel::ViewConfig hideVacm;
hideVacm.name = "everything";
hideVacm.subtree = "1.3.6.1.6.3.16";
hideVacm.included = false;
config.vacm.views.push_back(hideVacm);

rapidpixel::GroupConfig reader;
reader.securityName = "publicreader";
reader.groupName = "readers";
reader.community = true;            // An SNMPv2c community, not a USM user.
config.vacm.groups.push_back(reader);

rapidpixel::AccessConfig grant;
grant.groupName = "readers";
grant.level = rapidpixel::SecurityLevel::NoAuthNoPriv;
grant.readView = "everything";      // No writeView: read-only.
config.vacm.access.push_back(grant);

Rules of the access control:

  • Views. All ViewConfig entries with the same name form one view. An excluded family under an included one removes that branch from the view, as in the example above.
  • Security level. A grant applies to requests at its own security level and above: a grant at AuthPriv does not cover an AuthNoPriv request.
  • Notify views. AccessConfig::notifyView filters the notifications sent to a target (RFC 3413 3.3). The view is selected per target, from the security name its community maps to, under the SNMPv2c security model at NoAuthNoPriv. An empty notify view (the default) filters nothing. This differs from the literal reading of RFC 3413, where an empty view admits nothing, so that configurations without notify views keep sending notifications. A non-empty notify view must contain the OID of the notification and every object it carries (coldStart and authenticationFailure included); otherwise the notification is not sent to that target and is counted in notificationsSuppressed. A notify view on a grant above NoAuthNoPriv is a configuration fault when the community of a target maps into the group of that grant and the group has no grant at NoAuthNoPriv: the target can never select the view, so the view would filter nothing.
  • Security tables. Write access to 1.3.6.1.6.3.16 (VACM tables) is administrative access to the whole agent: whoever can write vacmSecurityToGroupTable can put any security name into any group. Write access to 1.3.6.1.6.3.15 (USM tables) allows creating and deleting users. Exclude both subtrees from the views of principals that are not administrators.
  • Users created by a manager. A user created over SNMP has keys but no rights: every request it sends is refused until a GroupConfig or a row of vacmSecurityToGroupTable names it. A group membership may name a user that does not exist yet.

FIPS operation

SnmpServer is not a validated cryptographic module and has no FIPS 140-3 certificate. In FIPS operation every cryptographic operation of SNMPv3 (HMAC-SHA-2, the SHA-2 digests of key localization, AES and random numbers) is performed by the OpenSSL FIPS provider through CryptoProvider; SnmpServer lies outside the cryptographic boundary of that module. Whether a product may claim FIPS 140-3 operation depends on the validated module, its certificate, version and security policy, and the operating environment, and must be determined for the product.

FIPS operation needs the following:

  • CMake option ${PARENT}_SNMPSERVER_WITH_FIPS ON. It turns SNMPv3 on.
  • The OpenSSL backend of CryptoProvider (OpenSSL 3.0 or newer): ${PARENT}_CRYPTOPROVIDER_BACKEND OPENSSL, or AUTO on Linux when OpenSSL 3 is found. On Windows AUTO selects CNG, so OPENSSL must be set. Configuration stops with an error for another backend.
  • CMake option ${PARENT}_SNMPSERVER_FIPS_MODULE_VERSION set to the version of the validated module (default 3.1.2). A module of another version is refused at run time.
  • A validated OpenSSL FIPS module installed on the target system. It is not part of the library.
  • The configuration fields:
config.fipsEnabled = true;
config.fipsModuleDir = "/opt/app/ossl-modules";   // Folder of fips.so.
config.fipsConfigFile = "/opt/app/openssl.cnf";   // Includes fipsmodule.cnf.

fipsConfigFile is an OpenSSL configuration file that includes fipsmodule.cnf, not fipsmodule.cnf itself. After the module is copied to the target system, the two files are created as follows:

# 1. Compute the installation data of the module and run its self-tests.
openssl fipsinstall -pedantic \
    -module /opt/app/ossl-modules/fips.so \
    -out    /opt/app/fipsmodule.cnf

# 2. Write the configuration file that includes it.
cat > /opt/app/openssl.cnf <<'CNF'
config_diagnostics = 1
openssl_conf = openssl_init

.include /opt/app/fipsmodule.cnf

[openssl_init]
providers = provider_sect

[provider_sect]
fips = fips_sect
CNF

-pedantic writes the approved-mode settings security-checks = 1 and drbg-no-trunc-md = 1 into fipsmodule.cnf. The library refuses a module without them (SecurityChecksDisabled, TruncDigestAllowed). The setting tls1-prf-ems-check applies to TLS only and is not required. Both paths may be empty to use the default locations of OpenSSL.

init(…) checks the module: it must load, pass its self-tests, have the accepted version and have the approved-mode settings on. These values are read from the module. If a check fails, init(…) fails with a configuration fault for fipsEnabled, getFipsState() returns Error and getFipsFailureReason() the reason. There is no fallback to the standard mode. Every protocol the library supports is approved, so no user is refused or downgraded in FIPS operation for its algorithms. CryptoProvider runs the module in its own OpenSSL library context with the fips and base providers and reports FIPS operation only when the context enforces it (SHA2-256 is available and MD5 is not); the OpenSSL state of the host application is not changed.

If the module becomes unusable after init(…) (a failed runFipsSelfTest(), or a failed self-test run by another component in the process), every cryptographic operation is refused and the agent answers no SNMPv3 message. SNMPv2c requests use no cryptography and are still answered.

CryptoProvider loads the FIPS module once per process and shares it with every component that uses CryptoProvider:

  • The first SnmpServer that requests FIPS operation determines the module paths. Another SnmpServer in the same process that names other paths fails with ModulePathConflict.
  • If another component has loaded the module first, the request of SnmpServer is served by that module when the paths match (an empty path accepts the module in force) and the module passes the version and settings checks.
  • On a Linux system with the kernel FIPS mode enabled (/proc/sys/crypto/fips_enabled is 1), CryptoProvider loads the module from the default locations of OpenSSL the first time any component uses it. Call init(…) with fipsEnabled before other components use CryptoProvider, or leave both paths empty.

At run time, getFipsState(), getFipsFailureReason(), getFipsModuleVersion() and getFipsSessionIndicator() report the state. Operational together with an approved indicator means approved operation for that message.

snmpCommunityTable

RFC 3584 5 defines snmpCommunityTable (1.3.6.1.6.3.18), which publishes the mapping of communities to security names that the agent applies to SNMPv2c requests. The table is served when serveCommunityTable is TRUE:

config.serveCommunityTable = true;    // Default is false.
config.engineIdSerial = "unit-0001";  // Required with serveCommunityTable.

Rules:

  • An engine identifier is required. The column snmpCommunityContextEngineID has the syntax SnmpEngineID (OCTET STRING (SIZE(5..32))) and defaults to the snmpEngineID of the agent. Without engineIdSerial, init(…) reports a fault. snmpCommunityContextName and snmpCommunityTransportTag are empty: the default context and no transport tag.
  • The table is off by default. The column snmpCommunityName contains the community string, so serving the table gives the communities to every manager whose view includes it. When the table is served, exclude 1.3.6.1.6.3.18 from every view of an untrusted principal, as the USM (.15) and VACM (.16) tables:

    rapidpixel::ViewConfig hideCommunities;
    hideCommunities.name = "everything";
    hideCommunities.subtree = "1.3.6.1.6.3.18";
    hideCommunities.included = false;
    config.vacm.views.push_back(hideCommunities);
    
  • Every column is read-only. The compliance statement of RFC 3584 permits it, and a writable snmpCommunitySecurityName would let a manager map a community to a security name with more rights. A SET is answered notWritable for existing and for new rows (RFC 3416 4.2.5).
  • The row index is a position, not the community. The INDEX is part of the OID of every cell, so an index derived from the community would publish it to a manager that may walk the table but not read its second column. The rows are named community1, community2 and so on, in configuration order.
  • The transport tag is empty, which RFC 3584 5.2.1 defines as accepted from any transport address: the agent applies no address filter per community.

Confirmed notifications

A trap is sent once; if the datagram is lost, neither side knows. An InformRequest (RFC 3416 4.2.7) is kept until the receiver acknowledges it and is retransmitted until the acknowledgment arrives or the retries are used up. Confirmation is set per target:

rapidpixel::TrapTargetConfig target;
target.address   = "192.0.2.5";
target.port      = 162;
target.community = readFirstLine("/path/to/trap-community");
target.confirmed = true;    // InformRequest instead of a trap.
target.timeoutMs = 5000;    // Time to wait for the acknowledgment.
target.retries   = 3;       // Retransmissions after the first attempt.
config.trapTargets.push_back(target);

The same sendNotification(…) call serves both kinds of target, and the coldStart notification sent by start() is confirmed for a confirmed target too. The counters of AgentStatistics show the result:

Table 36 - Statistics of confirmed notifications.

Counter Meaning
informsAcknowledged The receiver acknowledged.
informsPending Waiting for an acknowledgment at this moment.
informsAbandoned Given up: no acknowledgment within timeoutMs * (retries + 1), not sent because the queue was full, or still pending when the agent stopped. The counter to monitor.
informRetransmits Attempts after the first. A rising value while informsAbandoned stays at zero shows a path that loses datagrams.
notificationsSuppressed Not sent because a notify view excluded the notification (RFC 3413 3.3).

Details:

  • A retransmission sends the same octets as the first attempt, not a rebuilt message, so sysUpTime.0 is the same and the receiver does not see a new event.
  • An acknowledgment is matched by its request-id and its source address. A receiver that answers from another address (for example behind a load balancer) is not matched: the inform is retransmitted until the retries are used up and counted in informsAbandoned.
  • At most 64 informs wait for an acknowledgment at the same time, in one queue for all targets. A further inform is not sent: it is counted in informsAbandoned, and sendNotification(…) returns FALSE with the reason when no other target received the notification. timeoutMs * (retries + 1) may not exceed one hour, so a full queue empties within that time.

Response size

SNMPv3. A response never exceeds the msgMaxSize declared in the request, which is the largest message the manager can accept (RFC 3412 6.3). A GETBULK response is shortened to fewer repetitions, and a GET whose response does not fit is answered tooBig with an empty varbind list (RFC 3416 4.2.1). The agent declares a msgMaxSize of 65507 octets in its own messages: the largest UDP payload over IPv4.

Local limit. AgentConfig::maxResponseOctets limits the response size for SNMPv2c and SNMPv3 (at least 484 octets, or 0 for no limit). A SET whose response would not fit is answered tooBig and not applied.

Amplification. The source address of a UDP datagram is not verified, so a small request with a forged source address produces a larger response sent to the forged address. The library bounds the response: at most 32 varbinds per response and at most 512 octets per value, whatever max-repetitions requests. SNMPv2c has no msgMaxSize, so these bounds and maxResponseOctets are the only limits for SNMPv2c. If the agent is reachable from a network the operator does not control:

  • bind the listener to a management interface instead of 0.0.0.0;
  • configure no SNMPv2c community and use SNMPv3 with authentication, so that an unauthenticated request receives only a short Report;
  • set maxResponseOctets;
  • limit the rate of UDP port 161 in the network.

Limitations

Table 37 - Limitations.

Limitation Description
SNMPv1 is not supported. Only SNMPv2c and SNMPv3. SNMPv1 has no GETBULK and no Counter64.
vacmAccessTable and vacmViewTreeFamilyTable are read-only over SNMP. The compliance statement of RFC 3415 permits it. An access entry holds the view names, so a manager able to write it could widen its own view. The access policy is set in AgentConfig::vacm.
Removing a protocol of a user in usmUserTable cannot be undone. Setting usmUserAuthProtocol to usmNoAuthProtocol or usmUserPrivProtocol to usmNoPrivProtocol wipes the key. A SET that does this and then fails to apply a later varbind is answered undoFailed (RFC 3416 4.2.5), because the wiped key cannot be restored. Authentication cannot be removed while the row still has privacy: setting usmUserAuthProtocol to usmNoAuthProtocol is then answered inconsistentValue (RFC 3414 5), so a manager removes privacy first, in an earlier SET.
AES-192 and AES-256 require a long enough digest. AES-192 and AES-256 are not standardized for USM; implementations differ in how they extend a localized key that is shorter than the cipher key. The library does not extend keys: init(…) refuses a user whose authentication digest is shorter than the privacy key. Use AES-192 with HMAC-SHA-224 or stronger and AES-256 with HMAC-SHA-256 or stronger.
No MD5, SHA-1 or DES. HMAC-MD5-96, HMAC-SHA-96 and DES-CBC of RFC 3414 are not FIPS-approved and are not implemented. A manager configured for them gets a wrong digest or a decryption error. Configure SHA-2 and AES.
HMAC-SHA-224 is not available with Windows CNG. Windows CNG has no SHA-224, so init(…) refuses a user with AuthProtocol::HmacSha224 on the CNG backend of CryptoProvider. Use HMAC-SHA-256 or stronger, or the OpenSSL backend.
Standalone implementations of CryptoProvider. On Linux without OpenSSL 3, CryptoProvider uses its standalone implementations. They are not a validated module, and the time of their AES operations depends on the key and the data. A FIPS build never uses them.
FIPS operation needs the OpenSSL backend and a separately installed module. The FIPS provider requires OpenSSL 3.0 or newer. The module is installed on the target system, because its installation data is computed there.
One worker thread. The interface allows concurrent handler calls, so a thread pool can be added without an interface change. The library sets no stack size for the worker thread; handlers run on it.
Tables registered by the application have no row creation. Managers cannot create or delete rows of a table registered with registerTable(…).
A table indexed by an OBJECT IDENTIFIER causes two MIB checker warnings. The index has no size restriction and can exceed the OID length limit. The standard SNMP-VIEW-BASED-ACM-MIB causes the same warnings for vacmViewTreeFamilySubtree.
A variable-length string INDEX needs a syntaxOverride with a SIZE. The maximum length is a property of the data, so the library does not choose one. emitMibModule(…) refuses the table and names the cause.
Addresses are numeric. Listener and target addresses must be numeric; host names are not resolved.

Fixed limits

The following limits are constants of the library, not of the protocol. The library refuses a configuration it could not serve when the configuration is applied.

Table 38 - Fixed limits.

Limit Value When exceeded
Octets in one value 512 setValue(…) returns FALSE. RFC 2578 7.1.2 permits 65535; the limit keeps the decoding of a datagram free of allocations.
Sub-identifiers in an OID 128 The registration is refused (RFC 2578 3.5).
Varbinds in one PDU 32 A request with more is answered tooBig with an empty varbind list (RFC 3416 4.2.1). A GETBULK response is limited to 32 varbinds (RFC 3416 4.2.3).
SNMPv3 users 32 Configuration fault for usmUsers; a user creation over SNMP is answered resourceUnavailable.
View families (ViewConfig entries) 64 Configuration fault for vacm.views.
Group memberships (GroupConfig entries) and access entries (AccessConfig entries) 32 each Configuration fault for vacm.groups or vacm.access.
Octets in a security name, group name or view name 32 Configuration fault for the entry.
Octets in a view mask 16 Configuration fault for the view (RFC 3415).
Octets in a USM user name 1 to 32 Configuration fault for the user.
Octets in a community 1 to 64 Configuration fault for the community. The number of communities and of targets is not limited.
Octets in the first line of a credential file 1024 Configuration fault naming the file.
Octets in a passphrase at least 8 Configuration fault for the user (RFC 3414 11.2).
Characters in a descriptor 64 The registration is refused.
Unacknowledged informs 64 One queue for all targets. A further inform is not sent and is counted in informsAbandoned.
timeoutMs * (retries + 1) of a confirmed target one hour Configuration fault for trapTargets.
retries of a confirmed target 255 Configuration fault for trapTargets.
Octets in the boots file 4096 The file is refused without being parsed and init(…) fails.
Consecutive objects in a walk whose values cannot be encoded 32 The walk ends with genErr.

Security

The library does the following:

  • Bounded input. The BER decoder, the SNMPv3 message codec and every file the library reads (boots file, passphrase and key files) have fixed limits and refuse larger input, so a datagram cannot make the library allocate memory of a size it declares.
  • Wiping of keys. Localized keys, passphrases and their copies are overwritten before their memory is released, on the error paths too.
  • Out of memory. A method that returns TRUE or FALSE returns FALSE with “out of memory” in getLastError() when an allocation fails, and the agent stays usable. The worker thread drops a datagram it cannot process and counts it in internalErrors.
  • Constant-time comparison. Digests and communities are compared in a time that does not depend on how many octets match.
  • Boots file. The file is written through a temporary file that is created exclusively, readable by its owner only and never through a symbolic link. A file that exists but cannot be read stops init(…) instead of resetting the counter, and so does a file whose last line lacks the line end that the library writes (a file cut short while being written), because a shortened number would be a smaller counter. A counter that cannot be written stops init(…) too.
  • Files. Paths are UTF-8 on every platform; on Windows long paths are supported. A path that contains a NUL character is refused. Files are opened with the close-on-exec flag, so processes started by the application do not inherit them. A relative path is resolved against the current folder.
  • Handlers. Exceptions of handlers are caught, so a handler error does not end the process.
  • Build options. The library, the tests, the example and the template are compiled as standard C++17 with a strict warning set and, with GCC and Clang, stack protector, _FORTIFY_SOURCE=2 (optimized builds), stack clash protection and control flow protection where the compiler supports them; with MSVC, /sdl, /GS and /guard:cf. Executables (and the library when it is built as a shared library) are linked with full RELRO and a non-executable stack (/DYNAMICBASE, /NXCOMPAT and /guard:cf with MSVC). No warning is treated as an error in the library, so an unknown compiler cannot break the build.
  • FIPS operation without fallback, see FIPS operation.

The application is responsible for the following:

  • SNMPv2c sends the community in clear text. Use SNMPv3 with authPriv over networks the operator does not control, and give SNMPv2c communities read access to the objects they need only.
  • Credentials. Read credentials from files that only the account of the agent can read (authPassphraseFile, privPassphraseFile, authKeyFile, privKeyFile), never write them in the source code and never ship default credentials.
  • Boot counter. Set engineBootsFile on writable storage that survives a restart, in a folder that exists; init(…) requires it when an SNMPv3 user authenticates and fails when the counter cannot be written.
  • Engine identifier. Set engineIdSerial to a value that is unique per system and stable across restarts, for example a serial number.
  • Algorithms. A manager that supports only MD5, SHA-1 or DES cannot use SNMPv3 with this library.
  • Access control. Exclude the USM, VACM and community tables (1.3.6.1.6.3.15, .16 and .18) from the views of principals that are not administrators.

Build and connect to your project

Requirements: CMake 3.13 or newer and a C++17 compiler. SNMPv3 (${PARENT}_SNMPSERVER_WITH_V3, ON by default) requires the CryptoProvider library. CryptoProvider uses Windows CNG on Windows and needs nothing else there; on Linux it uses OpenSSL 3, so install the OpenSSL 3 development files (for example libssl-dev), or it uses its standalone implementations (see Limitations). FIPS operation requires the OpenSSL backend and a validated FIPS module (see FIPS operation). A build without SNMPv3 needs no CryptoProvider. On Windows the library links ws2_32 and iphlpapi; on a target without 64-bit atomic instructions it links atomic.

Build stand alone

The CryptoProvider library must be in the same folder as SnmpServer (in the SDK they are the folders modules/Communication/SnmpServer and modules/Service/CryptoProvider). The folder structure must be:

<work folder>
    SnmpServer
    CryptoProvider

Typical commands to build the library, the tests, the example and the device template:

cd SnmpServer
mkdir build
cd build
cmake ..
make

If SnmpServer is the top-level CMake project and SNMPv3 is on, the main CMakeLists.txt file adds the folder ../CryptoProvider before the library. If the folder does not exist, CMake stops with an error. A build without SNMPv3 needs no CryptoProvider: cmake .. -D_SNMPSERVER_WITH_V3=OFF. On Windows use cmake --build . --config Release instead of make. The programs are built into the bin folder of the build folder.

Run the tests with ctest in the build folder (on Windows ctest -C Release). Every test program is registered with CTest. A test that needs something the computer does not provide (external SNMP command line tools, IPv6, SNMPv3 in the build or a FIPS module) exits with code 77 and CTest reports it as skipped, not as passed. The program SnmpServerTestAgent (a read-only SNMPv2c agent for manual checks) is built with the tests but is not run by CTest.

CMake options

Table 39 - CMake options of the repository. In the options below ${PARENT} is the prefix defined by the top-level project (empty if SnmpServer is the top-level project, so the stand alone option is, for example, _SNMPSERVER_WITH_V3). Stand alone, ${PARENT}_SUBMODULE_CACHE_OVERWRITE is OFF, so options given with -D on the command line are kept.

Option Default Description
${PARENT}_SNMPSERVER ON Build the library.
${PARENT}_SNMPSERVER_WITH_V3 ON SNMPv3: USM (RFC 3414) and the SNMPv3 part of VACM (RFC 3415). Requires CryptoProvider.
${PARENT}_SNMPSERVER_WITH_FIPS OFF FIPS 140-3 operation through the OpenSSL FIPS provider. Turns SNMPv3 on and requires the OpenSSL backend of CryptoProvider, see FIPS operation.
${PARENT}_SNMPSERVER_FIPS_MODULE_VERSION 3.1.2 Version of the OpenSSL FIPS provider the library accepts at run time. Must not be empty when FIPS operation is on.
${PARENT}_SNMPSERVER_WITH_MIB_EMITTER ON Generator of SMIv2 MIB modules (emitMibModule(…)). OFF makes the library smaller; the method then returns FALSE.
${PARENT}_SNMPSERVER_TEST ON stand alone, OFF included in other project Build the tests.
${PARENT}_SNMPSERVER_EXAMPLE ON stand alone, OFF included in other project Build the example.
${PARENT}_SNMPSERVER_TEMPLATE ON stand alone, OFF included in other project Build the device template SnmpServerDeviceTemplate.
${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF stand alone, ON included in other project Option of the top-level project. When ON, the options above are overwritten with their defaults; the top-level project sets it to OFF to set the options itself.
CMAKE_BUILD_TYPE Release (if not specified) Build type.
${PARENT}_CRYPTOPROVIDER_BACKEND AUTO Backend of CryptoProvider: AUTO (CNG on Windows; OpenSSL 3 on Linux if it is found, otherwise the standalone implementations), CNG, OPENSSL or CUSTOM (the standalone implementations). FIPS operation requires OPENSSL. It is an option of CryptoProvider: set it in the parent project, or pass -D_CRYPTOPROVIDER_BACKEND=<value> in a stand alone build.
OPENSSL_ROOT_DIR not set Root of the OpenSSL installation, for the OpenSSL backend of CryptoProvider (option of the CMake OpenSSL module).

The compile definitions SNMPSERVER_WITH_V3, SNMPSERVER_WITH_MIB_EMITTER and SNMPSERVER_V3_WITH_FIPS that the options produce are public definitions of the SnmpServer target. The public headers are the same in every build.

Connect to your project

The library depends on the CryptoProvider library only (for SNMPv3). To include SnmpServer as source code in a CMake project, follow the steps below, which add CryptoProvider as well. For example, start with this repository structure:

CMakeLists.txt
src
    CMakeLists.txt
    yourLib.h
    yourLib.cpp

Create a 3rdparty folder and copy the CryptoProvider and SnmpServer folders into it:

CMakeLists.txt
src
    CMakeLists.txt
    yourLib.h
    yourLib.cpp
3rdparty
    CryptoProvider
    SnmpServer

Create the file 3rdparty/CMakeLists.txt with the following content:

cmake_minimum_required(VERSION 3.13)

################################################################################
## 3RD-PARTY
## dependencies for the project
################################################################################
project(3rdparty LANGUAGES CXX)

################################################################################
## SETTINGS
## basic 3rd-party settings before use
################################################################################
# To inherit the top-level architecture when the project is used as a submodule.
SET(PARENT ${PARENT}_YOUR_PROJECT_3RDPARTY)
# Disable self-overwriting of parameters inside included subdirectories.
SET(${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF CACHE BOOL "" FORCE)

################################################################################
## CONFIGURATION
## 3rd-party submodules configuration
################################################################################
SET(${PARENT}_SUBMODULE_CRYPTOPROVIDER                  ON  CACHE BOOL "" FORCE)
if (${PARENT}_SUBMODULE_CRYPTOPROVIDER)
    SET(${PARENT}_CRYPTOPROVIDER                        ON  CACHE BOOL "" FORCE)
    SET(${PARENT}_CRYPTOPROVIDER_BACKEND                AUTO CACHE STRING "" FORCE)
    SET(${PARENT}_CRYPTOPROVIDER_TEST                   OFF CACHE BOOL "" FORCE)
    SET(${PARENT}_CRYPTOPROVIDER_EXAMPLE                OFF CACHE BOOL "" FORCE)
endif()

SET(${PARENT}_SUBMODULE_SNMPSERVER                      ON  CACHE BOOL "" FORCE)
if (${PARENT}_SUBMODULE_SNMPSERVER)
    SET(${PARENT}_SNMPSERVER                            ON  CACHE BOOL "" FORCE)
    SET(${PARENT}_SNMPSERVER_WITH_V3                    ON  CACHE BOOL "" FORCE)
    SET(${PARENT}_SNMPSERVER_WITH_FIPS                  OFF CACHE BOOL "" FORCE)
    SET(${PARENT}_SNMPSERVER_WITH_MIB_EMITTER           ON  CACHE BOOL "" FORCE)
    SET(${PARENT}_SNMPSERVER_TEST                       OFF CACHE BOOL "" FORCE)
    SET(${PARENT}_SNMPSERVER_EXAMPLE                    OFF CACHE BOOL "" FORCE)
    SET(${PARENT}_SNMPSERVER_TEMPLATE                   OFF CACHE BOOL "" FORCE)
endif()

################################################################################
## INCLUDING SUBDIRECTORIES
## Adding subdirectories according to the 3rd-party configuration
################################################################################
if (${PARENT}_SUBMODULE_CRYPTOPROVIDER)
    add_subdirectory(CryptoProvider)
endif()

if (${PARENT}_SUBMODULE_SNMPSERVER)
    add_subdirectory(SnmpServer)
endif()

This adds CryptoProvider and then SnmpServer to the project, in dependency order. The options are set before add_subdirectory(...). SnmpServer does not add CryptoProvider itself when it is included in another project: the parent project has to add it first, as above. Without it, the compilation stops with the error CryptoProvider.h: No such file or directory. For a build without SNMPv3, set ${PARENT}_SNMPSERVER_WITH_V3 OFF and ${PARENT}_SUBMODULE_CRYPTOPROVIDER OFF. The tests, the example and the device template are not built when SnmpServer is included in another project, unless their options are set. Your repository structure will look like:

CMakeLists.txt
src
    CMakeLists.txt
    yourLib.h
    yourLib.cpp
3rdparty
    CMakeLists.txt
    CryptoProvider
    SnmpServer

Add 3rdparty to the main CMakeLists.txt before the folder of the application that links SnmpServer:

add_subdirectory(3rdparty)

Link the library to your application target in src/CMakeLists.txt:

target_link_libraries(yourApplication PRIVATE SnmpServer)

Replace yourApplication with the actual target name, then include SnmpServer.h in your source files. Linking the SnmpServer target provides the include folder, the C++17 requirement, the compile definitions and the dependencies: CryptoProvider (with libcrypto of OpenSSL or bcrypt of Windows), the thread library (Threads::Threads) and, on Windows, ws2_32 and iphlpapi.

Example

The example (folder example) is a minimal SNMPv2c agent. It serves one scalar object whose value the library holds and one scaled real value read by a handler, prints the generated MIB module and runs until it is interrupted (SIGINT or SIGTERM). The community is read from the file named on the command line.

// SnmpServer example: a minimal SNMPv2c agent with one listener, one community
// and two scalar objects. It serves until SIGINT or SIGTERM and shows the order
// of the calls. SnmpServerDeviceTemplate/ shows a complete configuration with
// SNMPv3, access control, a table and notifications.
//
// The example uses the public API only.

#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <fstream>
#include <string>
#include <thread>
#include "SnmpServer.h"

using rapidpixel::AgentConfig;
using rapidpixel::CommunityConfig;
using rapidpixel::ConfigFault;
using rapidpixel::ListenerConfig;
using rapidpixel::ObjectDef;
using rapidpixel::SnmpScale;
using rapidpixel::SnmpServer;
using rapidpixel::SnmpType;
using rapidpixel::SnmpValue;

static std::atomic<bool> g_running{true};
static void onSignal(int) { g_running.store(false); }

/// The OID arc the objects are registered under. 99999 is a placeholder: use
/// your own IANA enterprise number. The library predefines none.
static const char* const ROOT{"1.3.6.1.4.1.99999"};

/// The value the read handler returns. It does not change after start-up, so
/// it needs no lock; a value that changes must be guarded, because handlers run
/// on the worker thread of the agent.
static double g_temperature{21.5};

/// Returns the first line of @p path without the line break and without
/// trailing spaces and tabs, as the library reads its passphrase files, or an
/// empty string if the file cannot be read. A
/// credential is kept in a file that only its owner can read, not in the source
/// code.
static std::string firstLine(const char* path)
{
    std::ifstream file(path);
    std::string line;
    std::getline(file, line);
    while (!line.empty() &&
           (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
    {
        line.pop_back();
    }

    return line;
}

int main(int argc, char** argv)
{
    if (argc != 2)
    {
        std::printf("usage: %s <community-file>\n"
                    "  the file's first line is the SNMPv2c community this agent answers to\n",
                    argv[0]);

        return 2;
    }

    std::signal(SIGINT, onSignal);
    std::signal(SIGTERM, onSignal);

    AgentConfig config;

    ListenerConfig listener;
    listener.address = "0.0.0.0";
    listener.port = 16101;              // Port 161 needs privileges.
    config.listeners.push_back(listener);

    CommunityConfig community;
    community.community = firstLine(argv[1]);
    if (community.community.empty())
    {
        std::printf("%s: cannot be read, or its first line is empty\n", argv[1]);

        return 2;
    }
    config.communities.push_back(community);

    config.sysDescr = "Example camera, SnmpServer example";
    config.sysName = "camera-01";
    config.sysContact = "device operator";
    config.sysLocation = "front door";

    config.mibModule.moduleName = "CAMERA-EXAMPLE-MIB";
    config.mibModule.identityName = "camExampleMIB";
    config.mibModule.rootOid = ROOT;
    config.mibModule.conformanceOid = std::string(ROOT) + ".99";
    config.mibModule.lastUpdated = "202609040000Z";
    config.mibModule.organization = "Example Organization";
    config.mibModule.contactInfo = "Contact the supplier of this device.";
    config.mibModule.description = "Objects this example agent serves.";
    config.mibModule.arcs.push_back({std::string(ROOT) + ".1", "camScalars"});

    SnmpServer agent;
    if (!agent.init(config))
    {
        for (const ConfigFault& fault : agent.getConfigFaults())
        {
            std::printf("config: %s[%zu] %s: %s\n", fault.field.c_str(), fault.index,
                        fault.entryName.c_str(), fault.reason.c_str());
        }

        return 1;
    }

    // Scalar whose value the library holds (push): the application updates it
    // with setValue(), and requests are answered from the stored value without
    // waiting for the application.
    ObjectDef uptime;
    uptime.oid = std::string(ROOT) + ".1.1";
    uptime.name = "camFramesCaptured";
    uptime.type = SnmpType::Counter32;
    uptime.description = "Frames captured since start-up.";
    if (!agent.registerObject(uptime) ||
        !agent.setValue(uptime.oid, SnmpValue::makeCounter32(0)))
    {
        std::printf("%s: %s\n", uptime.name.c_str(), agent.getLastError().c_str());

        return 1;
    }

    // Scalar read by a handler (pull), with a scaled value: the handler returns
    // 21.5 and the response carries 215 (SnmpScale::Deci).
    ObjectDef temperature;
    temperature.oid = std::string(ROOT) + ".1.2";
    temperature.name = "camSensorTemperature";
    temperature.type = SnmpType::Real;
    temperature.scale = SnmpScale::Deci;
    temperature.description = "Temperature of the imaging sensor.";
    temperature.units = "tenths of a degree Celsius";

    SnmpServer::ObjectHandlers handlers;
    handlers.read = [](SnmpValue& value)
    {
        value = SnmpValue::makeReal(g_temperature);

        return true;
    };
    if (!agent.registerObject(temperature, handlers))
    {
        std::printf("%s: %s\n", temperature.name.c_str(), agent.getLastError().c_str());

        return 1;
    }

    // Objects are registered between init() and start(). start() binds the
    // listener and begins answering; registration is refused after it.
    if (!agent.start())
    {
        std::printf("start: %s\n", agent.getLastError().c_str());

        return 1;
    }

    std::printf("agent on port %u\n", static_cast<unsigned>(agent.getBoundPort(0)));

    // The engine identifier is printed once at start-up, so that a change
    // across restarts can be seen in the log. It is empty here, because the
    // example sets no engineIdSerial.
    if (!agent.getEngineIdHex().empty())
    {
        std::printf("engine %s\n", agent.getEngineIdHex().c_str());
    }

    // The SMIv2 MIB module generated from the registrations above.
    std::string module;
    if (agent.emitMibModule(module))
    {
        std::printf("--- CAMERA-EXAMPLE-MIB ---\n%s", module.c_str());
    }

    // Update the pushed value every 40 ms. A Counter32 value is always
    // accepted; the result is checked anyway, and a refusal is reported once
    // rather than at every update.
    std::uint32_t frames{0};
    bool reported{false};
    while (g_running.load())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(40));
        if (!agent.setValue(uptime.oid, SnmpValue::makeCounter32(++frames)) && !reported)
        {
            std::printf("%s: %s\n", uptime.name.c_str(), agent.getLastError().c_str());
            reported = true;
        }
    }

    agent.stop();

    return 0;
}

Create a community file that only you can read and start the example with it (the program is SnmpServerExample in the bin folder of the build folder):

umask 077
head -c 18 /dev/urandom | base64 > community.txt
./SnmpServerExample community.txt

The agent answers SNMPv2c requests with this community on UDP port 16101. An SNMP manager can read camFramesCaptured.0 (1.3.6.1.4.1.99999.1.1.0) and camSensorTemperature.0 (1.3.6.1.4.1.99999.1.2.0) and the standard objects, for example sysDescr.0.

SnmpServerDeviceTemplate

The folder SnmpServerDeviceTemplate contains a larger program to copy from: a complete agent configuration for a simulated system with four channels. The system state is a structure that the main thread changes while the worker thread of the agent answers requests; there is no hardware access. The program shows:

  • an SNMPv3 user operator with authPriv (HMAC-SHA-256 and AES-128), passphrases read by the library from files, and a persistent boot counter (snmpserver-boots.dat);
  • an SNMPv2c community with read access to a view that excludes the USM and VACM tables, and a write view for the SNMPv3 user that excludes the VACM tables, so access control cannot be changed over the network. The read view of the community is also its notify view, so a notification that carries a USM or VACM object is not sent under the community;
  • scalar objects whose values the library holds, a scaled real value read by a handler, and a writable string with a SIZE check (SnmpWriteResult::WrongLength);
  • a table with sparse indexes (1, 2, 4 and 7) and columns numbered from 2 after the INDEX object, whose handlers take one mutex and whose write handler returns WrongValue and InconsistentValue;
  • a notification sent as an InformRequest when a channel faults, and the inform statistics printed at exit;
  • the generated MIB module written to the file CAMERA-TEMPLATE-MIB.txt. If the file cannot be written, the program reports it and keeps running.

The program takes the names of three files: the community, the authentication passphrase and the privacy passphrase, each in the first line of its file. It is built with the library stand alone (option ${PARENT}_SNMPSERVER_TEMPLATE) into the bin folder of the build folder:

umask 077
for name in community auth priv; do head -c 18 /dev/urandom | base64 > $name.txt; done
./SnmpServerDeviceTemplate community.txt auth.txt priv.txt

The program writes the MIB module into the current folder, prints the UDP port (16102) and the engine identifier, and answers requests until it is interrupted (SIGINT or SIGTERM). Informs are sent to 127.0.0.1:16162 with the community; without a receiver they are counted as abandoned after the retransmissions. At exit the program prints the request and notification counters.

Table 40 - What to replace when the template is copied.

Item Replace with
ROOT OID under your own IANA enterprise number (99999 is a placeholder).
config.mibModule Your module name, arcs, organization and contact information.
config.trapTargets Address, port and community of your notification receiver.
Credential files The place where your system keeps credentials, readable by the account of the agent only.
config.engineIdSerial A value that is unique per system and stable across restarts, for example a serial number.
DeviceState Your own system state, guarded by one mutex that every handler takes.

Table of contents