Commit 4f85ac5c authored by Jan Koester's avatar Jan Koester
Browse files

snmp3

parent e690c8f3
Loading
Loading
Loading
Loading
+6 −2
Changes for CMakeLists.txt: 6 added lines, 2 removed lines.
Original line number Diff line number Diff line
cmake_minimum_required(VERSION 3.16)
project(snmpdd VERSION 0.1.0 LANGUAGES CXX)

# snmpdd: sends the state of the system as SNMP notifications (traps,
# SNMPv2c or v1) to network management stations, every interval and on
# snmpdd: sends the state of the system as SNMP notifications (traps:
# SNMPv3 with authentication and encryption (USM), SNMPv2c or v1) to
# network management stations, every interval and on
# events. It collects over sysbus what storaged knows (RAID arrays, LVM
# volume groups, SMART) and syslite's services and mount units, and how
# full the mounted file systems are; mibs/SNMPDD-MIB.txt describes the
@@ -29,6 +30,7 @@ find_package(libconfplus REQUIRED)
find_package(tinyxml2 REQUIRED)

set(SNMPDD_SYSCONFDIR "/etc/snmpdd" CACHE PATH "configuration directory")
set(SNMPDD_STATEDIR "/var/lib/snmpdd" CACHE PATH "SNMPv3 engine ID and boots")
set(SNMPDD_MIBDIR "${CMAKE_INSTALL_FULL_DATADIR}/snmp/mibs" CACHE PATH "MIB directory")

# needs storaged/api.h (storaged-dev)
@@ -38,11 +40,13 @@ add_executable(snmpdd
    src/collect.cpp
    src/sender.cpp
    src/snmp.cpp
    src/usm.cpp
)
target_include_directories(snmpdd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_definitions(snmpdd PRIVATE
    SNMPDD_CONFIG="${SNMPDD_SYSCONFDIR}/snmpdd.yaml"
    SNMPDD_SETTINGS="${SNMPDD_SYSCONFDIR}/settings.xml"
    SNMPDD_STATE="${SNMPDD_STATEDIR}/engine"
)
target_link_libraries(snmpdd PRIVATE sysbus::sysbus netplus::netplus confplus::confplus tinyxml2::tinyxml2)

+5 −0
Changes for dist/snmpdd.yaml: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -14,3 +14,8 @@ snmpdd:
  # seconds statvfs() of all mounted file systems may take together (a
  # dead NFS server doesn't answer)
  statvfs_timeout: 5
  # SNMPv3: the engine ID of snmpdd, in hex (the NMS knows its users
  # under it). Empty: made up at the first start and kept in the state
  # file, which also counts the starts (engineBoots).
  engine_id: ""
  state: /var/lib/snmpdd/engine
+13 −4
Changes for include/snmpdd/api.h: 13 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -34,7 +34,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * snmpdd collects, every interval, what storaged knows about RAID
 * arrays, LVM volume groups and SMART, the state of syslite's services
 * and mount units and how full the mounted file systems are, and sends
 * it as SNMP notifications (traps, SNMPv2c or v1) to every target. Event
 * it as SNMP notifications (traps: SNMPv3 with authentication and
 * encryption, SNMPv2c or v1) to every target. Event
 * notifications go out at once: a new SMART warning (storaged), a failed
 * self-test, a unit that fails or comes back (syslite), an item whose
 * status changed since the last report (a disk getting too warm, an
@@ -47,14 +48,22 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *       report_smart_attributes (ATA attributes of every disk),
 *       fs_warning, fs_critical (% used), temp_warning, temp_critical
 *       (°C of a disk), service (repeated: the services
 *       reported; none: all), enterprise (read only); then per target:
 *       target (host), port, version (2c|1), community
 *   SetSettings {the same, without enterprise} replaces all of them
 *       reported; none: all), enterprise, engine_id (hex; read only);
 *       then per target: target (host), port, version (3|2c|1),
 *       version 1/2c: community; version 3: user, level (noAuthNoPriv|
 *       authNoPriv|authPriv), auth (SHA|SHA-256|SHA-384),
 *       auth_password_set, priv (AES|AES-256), priv_password_set --
 *       passwords are never sent back
 *   SetSettings {the same, without enterprise and engine_id; version 3:
 *       auth_password, priv_password instead of *_set: empty keeps the
 *       one the target (host, port, user) has}
 *       -- replaces all of them
 *
 * Status:
 *   GetStatus -> enabled, interval, last_report (unix time, 0: never),
 *       next_report (0: off), sent (notifications since start), status
 *       (ok|warning|critical|unknown: the worst item of the last report),
 *       engine_id, engine_boots (SNMPv3),
 *       problem? (repeated: what couldn't be collected); then per
 *       target: target (host:port), sent, error?
 *   SendReport -> sent (notifications) -- collects and sends now; an
+91 −2
Changes for src/config.cpp: 91 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <tinyxml2.h>

#include "lang.h"
#include "usm.h"

namespace snmpdd {

@@ -116,6 +117,12 @@ Config loadConfig(const std::string &path) {
        }
        if (readValue(conf, "/snmpdd/statvfs_timeout", v) && toInt(v, n) && n > 0 && n <= 300)
            cfg.statvfsTimeout = (int)n;
        if (readValue(conf, "/snmpdd/engine_id", v) && !v.empty()) {
            std::string id;
            if (!usm::fromHex(v, id)) throw std::runtime_error(path + ": engine_id: 5 to 32 bytes in hex");
            cfg.engineId = id;
        }
        if (readValue(conf, "/snmpdd/state", v) && !v.empty()) cfg.statePath = v;
    } catch (const confplus::ConfException &e) {
        throw std::runtime_error(path + ": " + e.what());
    }
@@ -165,6 +172,12 @@ bool loadSettings(const std::string &path, Settings &out, std::string *error) {
            e->QueryIntAttribute("port", &t.port);
            if (e->Attribute("version")) t.version = e->Attribute("version");
            if (e->Attribute("community")) t.community = e->Attribute("community");
            if (e->Attribute("user")) t.user = e->Attribute("user");
            if (e->Attribute("level")) t.level = e->Attribute("level");
            if (e->Attribute("auth")) t.auth = e->Attribute("auth");
            if (e->Attribute("auth_password")) t.authPassword = e->Attribute("auth_password");
            if (e->Attribute("priv")) t.priv = e->Attribute("priv");
            if (e->Attribute("priv_password")) t.privPassword = e->Attribute("priv_password");
            out.targets.push_back(t);
        }
    std::string bad = validate(out);
@@ -205,7 +218,18 @@ bool saveSettings(const std::string &path, const Settings &s, std::string *error
        e->SetAttribute("host", t.host.c_str());
        e->SetAttribute("port", t.port);
        e->SetAttribute("version", t.version.c_str());
        if (t.version != "3") {
            e->SetAttribute("community", t.community.c_str());
            continue;
        }
        e->SetAttribute("user", t.user.c_str());
        e->SetAttribute("level", t.level.c_str());
        if (t.level == "noAuthNoPriv") continue;
        e->SetAttribute("auth", t.auth.c_str());
        e->SetAttribute("auth_password", t.authPassword.c_str());
        if (t.level != "authPriv") continue;
        e->SetAttribute("priv", t.priv.c_str());
        e->SetAttribute("priv_password", t.privPassword.c_str());
    }

    tinyxml2::XMLPrinter printer;
@@ -258,12 +282,77 @@ std::string validate(const Settings &s) {
    for (auto &t : s.targets) {
        if (!validHost(t.host)) return tr("invalid target host \"%1\"", "ungültiger Zielhost \"%1\"", {t.host});
        if (t.port < 1 || t.port > 65535) return tr("invalid port for %1", "ungültiger Port für %1", {t.host});
        if (t.version != "2c" && t.version != "1")
            return tr("SNMP version for %1: 2c or 1", "SNMP-Version für %1: 2c oder 1", {t.host});
        if (t.version != "2c" && t.version != "1" && t.version != "3")
            return tr("SNMP version for %1: 3, 2c or 1", "SNMP-Version für %1: 3, 2c oder 1", {t.host});
        if (t.version != "3") {
            if (t.community.empty() || t.community.size() > 255)
                return tr("community for %1: 1 to 255 characters", "Community für %1: 1 bis 255 Zeichen", {t.host});
            continue;
        }
        if (t.user.empty() || t.user.size() > 32)
            return tr("user for %1: 1 to 32 characters", "Benutzer für %1: 1 bis 32 Zeichen", {t.host});
        if (t.level != "noAuthNoPriv" && t.level != "authNoPriv" && t.level != "authPriv")
            return tr("security level for %1: noAuthNoPriv, authNoPriv or authPriv",
                      "Sicherheitsstufe für %1: noAuthNoPriv, authNoPriv oder authPriv", {t.host});
        if (t.level == "noAuthNoPriv") continue;
        usm::Auth a;
        if (!usm::parseAuth(t.auth, a))
            return tr("authentication for %1: SHA, SHA-256 or SHA-384", "Authentifizierung für %1: SHA, SHA-256 oder SHA-384", {t.host});
        // RFC 3414 11.2: at least 8 characters
        if (t.authPassword.size() < 8 || t.authPassword.size() > 255)
            return tr("authentication password for %1: 8 to 255 characters",
                      "Authentifizierungs-Passwort für %1: 8 bis 255 Zeichen", {t.host});
        if (t.level == "authNoPriv") continue;
        usm::Priv p;
        if (!usm::parsePriv(t.priv, p))
            return tr("encryption for %1: AES or AES-256", "Verschlüsselung für %1: AES oder AES-256", {t.host});
        if (p == usm::Priv::Aes256 && a == usm::Auth::Sha1)
            return tr("AES-256 for %1 needs SHA-256 or SHA-384", "AES-256 für %1 braucht SHA-256 oder SHA-384", {t.host});
        if (t.privPassword.size() < 8 || t.privPassword.size() > 255)
            return tr("encryption password for %1: 8 to 255 characters",
                      "Verschlüsselungs-Passwort für %1: 8 bis 255 Zeichen", {t.host});
    }
    return "";
}

std::string Target::key() const {
    bool v6 = host.find(':') != std::string::npos;
    return (v6 ? "[" + host + "]" : host) + ":" + std::to_string(port);
}

bool startEngine(const Config &c, std::string &id, uint32_t &boots, std::string *error) {
    // the state file: "engine_id <hex>" and "boots <n>"
    std::string storedId;
    unsigned long long storedBoots = 0;
    if (FILE *f = fopen(c.statePath.c_str(), "re")) {
        char key[32], value[128];
        while (fscanf(f, "%31s %127s", key, value) == 2) {
            if (!strcmp(key, "engine_id")) usm::fromHex(value, storedId);
            else if (!strcmp(key, "boots")) storedBoots = std::strtoull(value, nullptr, 10);
        }
        fclose(f);
    }
    uint32_t pen = c.enterprise.arcs.size() > 6 && c.enterprise.arcs[4] == 4 && c.enterprise.arcs[5] == 1
                       ? c.enterprise.arcs[6] : 0;
    id = !c.engineId.empty() ? c.engineId : !storedId.empty() ? storedId : usm::newEngineId(pen);
    // RFC 3414 2.2.2: a new engine ID starts at 1; 2^31-1 stays
    boots = id != storedId ? 1 : storedBoots >= 2147483647ull ? 2147483647u : (uint32_t)storedBoots + 1;

    std::string dir = c.statePath.substr(0, c.statePath.rfind('/') == std::string::npos ? 0 : c.statePath.rfind('/'));
    if (!dir.empty()) mkdir(dir.c_str(), 0700);
    std::string tmp = c.statePath + ".new";
    FILE *f = fopen(tmp.c_str(), "we");
    bool ok = f && fprintf(f, "engine_id %s\nboots %u\n", usm::toHex(id).c_str(), boots) > 0;
    if (f) {
        ok = ok && fflush(f) == 0 && fsync(fileno(f)) == 0;
        fclose(f);
    }
    if (!ok || rename(tmp.c_str(), c.statePath.c_str()) != 0) {
        if (error) *error = c.statePath + ": " + strerror(errno);
        unlink(tmp.c_str());
        return false;
    }
    return true;
}

} // namespace snmpdd
+26 −3
Changes for src/config.h: 26 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -43,7 +43,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *     <filesystems warning="85" critical="95"/>
 *     <disks temp_warning="50" temp_critical="60" attributes="yes"/>
 *     <services><service>sshd</service></services>   (none: all)
 *     <targets><target host="nms" port="162" version="2c" community="public"/></targets>
 *     <targets>
 *       <target host="nms" port="162" version="2c" community="public"/>
 *       <target host="nms2" port="162" version="3" user="snmpdd" level="authPriv"
 *               auth="SHA-256" auth_password="..." priv="AES" priv_password="..."/>
 *     </targets>
 *   </snmpdd> */

#pragma once
@@ -60,13 +64,27 @@ struct Config {
    snmp::Oid enterprise{1, 3, 6, 1, 4, 1, 99999};
    size_t maxMessage = 1472;   // bytes of one notification (UDP payload)
    int statvfsTimeout = 5;     // seconds for all file systems together
    // SNMPv3: the engine ID ("" : made up once and kept in the state
    // file) and where engineBoots is counted
    std::string engineId;
    std::string statePath = SNMPDD_STATE;
};

struct Target {
    std::string host;
    int port = 162;
    std::string version = "2c";  // 2c or 1
    std::string community = "public";
    std::string version = "2c";  // 2c, 1 or 3
    std::string community = "public";  // 1, 2c
    // 3 (USM): level noAuthNoPriv, authNoPriv or authPriv; auth SHA,
    // SHA-256 or SHA-384; priv AES or AES-256 (this one with SHA-256/384)
    std::string user;
    std::string level = "authPriv";
    std::string auth = "SHA-256";
    std::string authPassword;
    std::string priv = "AES";
    std::string privPassword;

    std::string key() const;  // host:port, as in the status
};

struct Settings {
@@ -93,4 +111,9 @@ bool saveSettings(const std::string &path, const Settings &s, std::string *error
// "" or what is wrong (in the language of the call, see tr())
std::string validate(const Settings &s);

// The SNMPv3 engine: the ID (c.engineId, else the one in the state file,
// else a new one) and engineBoots, one more than last time; both written
// back to the state file.
bool startEngine(const Config &c, std::string &id, uint32_t &boots, std::string *error);

} // namespace snmpdd
Loading