Ich bin auf der Suche nach einem Monitoring Plugin für den Spam- und Virenfilter für Mails rspamd. Das ist ziemlich ernüchtern da ich außer dem check_tcp
auf die verwendeten Ports nichts gefunden habe. Zufriedenheit sieht anders aus. Zumal rspamd eine schöne Statstik hat: rspamc stat
.
root@mx01 ~ # rspamc stat Results for command: stat (0.000 seconds) Messages scanned: 33384 Messages with action reject: 687, 2.05% Messages with action soft reject: 0, 0.00% Messages with action rewrite subject: 0, 0.00% Messages with action add header: 228, 0.68% Messages with action greylist: 868, 2.60% Messages with action no action: 31601, 94.65% Messages treated as spam: 915, 2.74% Messages treated as ham: 32469, 97.25% Messages learned: 8 Connections count: 25912 Control connections count: 26760 Pools allocated: 536378 Pools freed: 536354 Bytes allocated: 46.83MiB Memory chunks allocated: 93 Shared chunks allocated: 16 Chunks freed: 0 Oversized chunks: 2424 Fuzzy hashes in storage "rspamd.com": 1271695026 Fuzzy hashes stored: 1271695026 Statfile: BAYES_SPAM type: redis; length: 0; free blocks: 0; total blocks: 0; free: 0.00%; learned: 0; users: 1; languages: 0 Statfile: BAYES_HAM type: redis; length: 0; free blocks: 0; total blocks: 0; free: 0.00%; learned: 10; users: 1; languages: 0 Total learns: 10
Richtig schön: Mit dem Argument --compact
erhält man das ganze als JSON-String. Perfekte Grundlage für einen schönen Check. Nur schade, dass ich nichts passendes gefunden habe.
Und wie übersetzt man das nach Monitoring?
Die Daten sind da. Im Falle von rspamd war es leicht diese zu bekommen. Genau genommen ist das aber egal ob jetzt rspamd oder irgendeine andere Software her halten muss. Letztendlich geht es in diesem Beitrag darum Daten von egal wo her via SNMP z.B. einem Monitoringsystem wie Nagios, Icinga oder was auch immer zur Verfügung zu stellen.
Unter Linux dürfte die Wahl beim SNMP-Server auf snmpd fallen. In einem vorherigen Artikel habe ich schon einmal gezeigt wie man vorhandene Checks via SNMP verwenden kann: Monitoring nach Remote. Nur ist dieses Mal die Ausgangslage etwas anders: Es gibt die gewünschte Statistik, jedoch fehlt der passende Check.
Deswegen möchte ich dieses mal etwas anders an das Problem heran gehen. Mittels PEN (Private Enterprise Number) „übersetze“ ich die JSON-Daten passend für SNMP. Den eigentlichen Check fürs Monitoring übernimmt ein alter Bekannter: check_snmp.
JSON nach SNMP
Eine PEN habe ich bereits – siehe Link oben: Dort könnte eine kostenfrei registriert werden. Also, meine PEN ist wie gesagt vorhanden und lautet .1.3.6.1.4.1.41305. Im nächsten Schritt werfen wir einen Blick in die Dokumentation von snmpd.conf:
EXTENDING AGENT FUNCTIONALITY One of the first distinguishing features of the original UCD suite was the ability to extend the functionality of the agent - not just by recompiling with code for new MIB modules, but also by configuring the running agent to report additional information. There are a number of techniques to support this, including: * running external commands (exec, extend, pass) * loading new code dynamically (embedded perl, dlmod) * communicating with other agents (proxy, SMUX, AgentX)
Na bitte, da geht doch was. Vor allem der Teil mit „pass“ ist interessant. Die Doku etwas weiter gelesen und alles was man braucht ist ein Skript welches 3 verschiedene Aufrufe bedienen kann:
- -s für „set“: Das fällt bei mir raus, ich liefere ja nur Statstik Daten zurück. Also keinerlei Einstellungen die jemand verändern wollte. Das ist also rasch erledigt 😉
- -g für „get“: Liefert zur gewünschten OID den passenden Wert. Oder nichts falls es diese OID nicht gibt. Klingt ebenfalls machbar.
- -n für „next“: Liefert anstatt dem Wert für diese OID den Wert der nächsten. Richtig spannend: Das funktioniert auch falls es die OID nicht gibt. Die übermittelte OID wird in diesem Falle nicht geprüft sondern lediglich die nächste OID, welche nach dieser kommen würde, wird genommen. Ok, das ist jetzt etwas aufwendiger. Die nächste OID. Aber auch da sollte einem was passendes einfallen. Spannend. Somit sicherlich lösbar. Irgendwie halt.
Das snmpd pass-through Skript
Also los geht es. Zuerst mal mache ich mich an das Skript für pass-through ran. Ich habe mir für Perl entschieden weil dort die Sache mit -n für next meiner Meinung nach elegant zu lösen ist. Hier tut es aber sicherlich jede andere Sprache mindestens genauso gut.
#!/usr/bin/perl # # Gehirn-Mag.Net: rspamc stat nach snmpd # # 2020-07-28, steffen: Init... use strict; use warnings; use JSON; use Storable qw(store retrieve); # Path to rspamc use constant RSPAMC => '/usr/bin/rspamc'; # disk cache file - for faster snmpwalk ;-) use constant CACHE => '/tmp/tm_rspamd.cache'; # seconds to keep in disk cache use constant KEEP => 10; # translate OID to JSON key use constant OID2KEY => { '.1.3.6.1.4.1.41305.1.1.1' => ['counter', 'scanned' ], '.1.3.6.1.4.1.41305.1.1.2' => ['counter', 'learned' ], '.1.3.6.1.4.1.41305.1.1.3' => ['counter', 'spam_count' ], '.1.3.6.1.4.1.41305.1.1.4' => ['counter', 'ham_count' ], '.1.3.6.1.4.1.41305.1.1.5' => ['counter', 'connections' ], '.1.3.6.1.4.1.41305.1.1.6' => ['counter', 'control_connections' ], '.1.3.6.1.4.1.41305.1.1.7' => ['counter', 'pools_allocated' ], '.1.3.6.1.4.1.41305.1.1.8' => ['counter', 'pools_freed' ], '.1.3.6.1.4.1.41305.1.1.9' => ['integer', 'bytes_allocated' ], '.1.3.6.1.4.1.41305.1.1.10' => ['integer', 'chunks_allocated' ], '.1.3.6.1.4.1.41305.1.1.11' => ['integer', 'shared_chunks_allocated' ], '.1.3.6.1.4.1.41305.1.1.12' => ['integer', 'chunks_freed' ], '.1.3.6.1.4.1.41305.1.1.13' => ['integer', 'chunks_oversized' ], '.1.3.6.1.4.1.41305.1.1.14' => ['integer', 'fragmented' ], '.1.3.6.1.4.1.41305.1.1.15' => ['counter', 'total_learns' ], '.1.3.6.1.4.1.41305.1.1.16.1' => ['counter', 'actions/reject' ], '.1.3.6.1.4.1.41305.1.1.16.2' => ['counter', 'actions/soft reject' ], '.1.3.6.1.4.1.41305.1.1.16.3' => ['counter', 'actions/rewrite subject' ], '.1.3.6.1.4.1.41305.1.1.16.4' => ['counter', 'actions/add header' ], '.1.3.6.1.4.1.41305.1.1.16.5' => ['counter', 'actions/greylist' ], '.1.3.6.1.4.1.41305.1.1.16.6' => ['counter', 'actions/no action' ], }; # helper function: get rspamc stat data sub getJson { # Get JSON data my $json; die RSPAMC . ' not found' unless -x RSPAMC; # read cache if exists and not to old if(-r CACHE && (stat CACHE)[9] + KEEP >= time) { $json = retrieve CACHE; } else { open PH, RSPAMC . ' stat --compact |' or die 'Woops: ' . $!; local $/; $json = decode_json <PH>; # freeze in cache file store $json, CACHE; close PH; } return $json; } # helper function: print result sub printOid { # get rspamc JSON, split key name and get value my $oid = shift; my @parts = split '/', OID2KEY->{$oid}[1]; my $json = getJson; printf "%s\n%s\n%s\n", $oid, OID2KEY->{$oid}[0], defined $parts[1] ? $json->{$parts[0]}{$parts[1]} : $json->{$parts[0]}; exit 0; } # get ARGS if set my $task = $ARGV[0] || ''; my $oid = $ARGV[1] || ''; # nothing to do? exit 0 unless $task; # set - not implemented. No need to do any more. if($task eq '-s') { print 'not-writeable', "\n"; exit 0; } # -g OID = get if($task eq '-g' && $oid) { # exit if unknown oid exit 0 unless defined OID2KEY->{$oid}; printOid $oid; } # -n OID = next if($task eq '-n' && $oid) { # build oid list. If searched oid is not included add it my @oids = keys OID2KEY; push @oids, $oid unless defined OID2KEY->{$oid}; # walk through sorted oids, found current and get next oid my @sorted = map { $_->[0] } sort { $a->[1] cmp $b->[1] } map { [$_, join '', map { sprintf "%8d", $_ || 0 } split /\./, $_] } @oids; my $next; while(my $item = shift @sorted) { if($item eq $oid) { $next = shift @sorted; last; } } # output if next is defined else just quit printOid $next if $next; exit 0; } # Öhm... should never ever happen exit 0;
Eine kurze Erklärung zu dem Skript:
- Zeile 17: Hier lege ich den Pfad zur rspamc fest.
- Zeile 19: Damit für einen snmpwalk nicht jedes mal via rspamc die momentanen Werte ermittelt werden müssen lege ich die ermittelten Werte für n Sekunden in dieser Datei ab. Die sollte vom snmpd somit beschrieben werden können.
- Zeile 21: Für diese n Sekunden den Dateninhalt der Cache-Datei aus Zeile 19 für gültig empfinden.
- Zeile 23 bis 45: Die Zuordnung der OIDs zur Einheit und dem Pfad im JSON-Objekt von rspamc. Als „Pfadtrenner“ habe ich / verwendet.
- Zeile 49: Diese Funktion entscheidet ob es, sofern vorhanden, die im Cache liegenden Daten verwenden kann oder die aktuellen Werte von rspamc abholt.
- Zeile 70: Funktion für die Ausgabe der gefundenen Werte in der Form wie in der snmpd-Doku verlangt.
- Zeile 93: -s für set gibt es in diesem Falle nicht. Fehlerausgabe wie in snmpd Doku beschrieben.
- Zeile 100: -g für get. Einfach den passenden Wert im JSON finden und ausgeben.
- Zeile 108: -n für next. Das hat mal kurz ein paar Momente Hirnschmalz gekostet. Zur Logik: Falls die als Argument erhaltende OID nicht in der definierten OID-Tabelle ab Zeile 23 zu finden ist wird diese in die Liste der OIDs einfach aufgenommen. Die Liste der OIDs wird nun sortiert. Das geht am einfachsten in dem man die Punkte entfernt und jeden Ziffernblock auf 8 Zeichen länge bringt. 8 Zeichen sollten genügend Reserve für alle möglichen Eventualtiäten sein. Die so generierten Strings lassen sich wunderbar sortieren. Nun noch rasch die übertragene OID in der Liste gesucht und einfach das nächste Element aus der Liste genommen. Falls es keines mehr gibt verhalten wie in der snmpd-Doku geschrieben.
- Der Rest: Kommentare und ein bisschen Geplänkel. Wenig spannendes.
So, damit ist Teil 1 erledigt. Das „Übersetzungsskript“ von rspamc nach snmpd ist fertig.
Ein bisschen Konfig muss sein
Dem nächsten Schritt welchem ich nachgehen möchte ist die Einrichtung des SNMP-Servers snmpd
. Deswegen hier kurz und knackig dessen Konfiguration:
# # Gehirn-Mag.Net Beispiel snmpd.conf fürs Monitoring # ####################################### # # Allgemeine Einstellungen # # Auf *:161 lauschen agentAddress udp:161 ####################################### # # Auth - 1x public mit read only alles # view public included .1 rocommunity public default -V public ####################################### # # Build in - ein paar Beispiele # # System Informationen sysLocation dsb RZ sysContact Hostmaster of the Day <hostmaster-of-the-day@gehirn-mag.net> sysName gmn sysDescr gmn Server Appliance sysServices 72 # Process Monitoring proc snmpd # Disk Monitoring disk / 20% includeAllDisks 10% # System Load load 12 10 5 ####################################### # # pass-through extensions # pass .1.3.6.1.4.1.41305.1.1 /usr/local/bin/tm_rspamd.pl
Das ist eine einfache Konfiguration welche der Community „public“ lesenden Zugriff auf alles gibt. Die Prozess- und Plattenüberachung sind Beiwerk und für das eigentliche Bespiel ohne Belang. Ob ich die jetzt so auf einem produktiven System einsetzen möchte ist ein anderes Thema. Interessant ist eigentlich nur die Zeile 45: Hier steht, dass via „pass“ (= pass-through) meine PEN OID über das bereits erstellte Perl-Skript eingebunden werden soll.
Zur besseren Struktuierung meiner PEN 41305 habe ich dort die Unternummer 1.1 hinten angefügt. Die erste 1 steht bei den mir verwendeten OIDs für „Email“, die zweite 1 für „rspamd“. Das ist aber einer Organisationsfrage der eigenen OIDs unterhalb der PEN-Nummer und bleibt jedem selbst überlassen wie man das realsieren will.
Unterm Strich bleibt folgende, wichtige Erkenntnis in diesem Abschnitt: Es braucht lediglich eine einzige Zeile in der smtpd Konfig um einen ganzen Unterbaum an OIDs an ein externen Programm bzw. Skript zu übergeben. Sind wir ehrlich zu einander: Das war jetzt wirklich einfach.
Die flotte Nummer zwischendurch
Ab diesem Zeitpunkt funktioniert ein snmpget bzw. ein snmpwalk. Alle Infos werden gelistet. Nur sind die OIDs noch numerisch und somit wenig aussagekräfig da man in diesem Fall genau wissen muss was welche OID letztenlich bedeuten soll. Der nächste Schritt ist somit nicht zwingend notwendig sondern dient lediglich der eigenen Bequemlichkeit: Die Erstellung einer MIB mit welcher aus den numerischen OIDs klingende Namen gemacht werden können.
dsb-its-MIB DEFINITIONS ::= BEGIN IMPORTS OBJECT-TYPE, NOTIFICATION-TYPE, MODULE-IDENTITY, Integer32, Opaque, enterprises, Counter32, Unsigned32 FROM SNMPv2-SMI; DsbIts MODULE-IDENTITY LAST-UPDATED "202007240000Z" ORGANIZATION "gehirn-mag.net" CONTACT-INFO "Śteffen Schoch dein@gehirn-mag.net" DESCRIPTION "rspamd 2 snmp" ::= { enterprises 41305 } -- .1.3.6.1.4.1.41305.1 TuxMail OBJECT IDENTIFIER ::= { gmn 1 } -- .1.3.6.1.4.1.41305.1.1 TmRspamd OBJECT IDENTIFIER ::= { rspamd 1 } -- .1.3.6.1.4.1.41305.1.1.1 TmScanned OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages scanned" ::= { TmRspamd 1 } -- .1.3.6.1.4.1.41305.1.1.2 TmLearned OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages learned" ::= { TmRspamd 2 } -- .1.3.6.1.4.1.41305.1.1.3 TmSpam OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages treated as spam" ::= { TmRspamd 3 } -- .1.3.6.1.4.1.41305.1.1.4 TmHam OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages treated as ham" ::= { TmRspamd 4 } -- .1.3.6.1.4.1.41305.1.1.5 TmConnections OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Connections count" ::= { TmRspamd 5 } -- .1.3.6.1.4.1.41305.1.1.6 TmControlConnections OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Control connections count" ::= { TmRspamd 6 } -- .1.3.6.1.4.1.41305.1.1.7 TmPoolsAllocated OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Pools allocated" ::= { TmRspamd 7 } -- .1.3.6.1.4.1.41305.1.1.8 TmPoolsFreed OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Pools freed" ::= { TmRspamd 8 } -- .1.3.6.1.4.1.41305.1.1.9 TmBytesAllocated OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "Bytes allocated" ::= { TmRspamd 9 } -- .1.3.6.1.4.1.41305.1.1.10 TmChunksAllocated OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "Memory chunks allocated" ::= { TmRspamd 10 } -- .1.3.6.1.4.1.41305.1.1.11 TmSharedChunksAllocated OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "Shared chunks allocated" ::= { TmRspamd 11 } -- .1.3.6.1.4.1.41305.1.1.12 TmChunksFreed OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "Chunks freed" ::= { TmRspamd 12 } -- .1.3.6.1.4.1.41305.1.1.13 TmChunksOversized OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "Oversized chunks" ::= { TmRspamd 13 } -- .1.3.6.1.4.1.41305.1.1.14 TmFragmented OBJECT-TYPE SYNTAX INTEGER MAX-ACCESS read-only STATUS current DESCRIPTION "Fragmented" ::= { TmRspamd 14 } -- .1.3.6.1.4.1.41305.1.1.15 TmTotalLearns OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages learned" ::= { TmRspamd 15 } -- .1.3.6.1.4.1.41305.1.1.16 TmActions OBJECT IDENTIFIER ::= { TmRspamd 16 } -- .1.3.6.1.4.1.41305.1.1.16.1 TmActionReject OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages with action reject" ::= { TmActions 1 } -- .1.3.6.1.4.1.41305.1.1.16.2 TmActionSoftReject OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages with action soft reject" ::= { TmActions 2 } -- .1.3.6.1.4.1.41305.1.1.16.3 TmActionRewriteSubject OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages with action rewrite subject" ::= { TmActions 3 } -- .1.3.6.1.4.1.41305.1.1.16.4 TmActionAddHeader OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages with action soft add header" ::= { TmActions 4 } -- .1.3.6.1.4.1.41305.1.1.16.5 TmActionGreylist OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages with action greylist" ::= { TmActions 5 } -- .1.3.6.1.4.1.41305.1.1.16.6 TmActionNoAction OBJECT-TYPE SYNTAX COUNTER MAX-ACCESS read-only STATUS current DESCRIPTION "Messages with action no action" ::= { TmActions 6 } END
Das ist mehr oder weniger Fleißarbeit. Nur mit dieser MIB-Datei an der richtigen Stelle abgelegt und schon hat die Suche bzw. die Ausgabe eine viel schönere Optik:
[steffen@gmn ~]# snmpwalk -v2c -cpublic -m ALL localhost .1.3.6.1.4.1.55286 gmn::TmScanned = Counter32: 8920 gmn::TmLearned = Counter32: 0 gmn::TmSpam = Counter32: 145 gmn::TmHam = Counter32: 8775 gmn::TmConnections = Counter32: 4995 gmn::TmControlConnections = Counter32: 96 gmn::TmPoolsAllocated = Counter32: 5112 gmn::TmPoolsFreed = Counter32: 5091 gmn::TmBytesAllocated = INTEGER: 23999888 gmn::TmChunksAllocated = INTEGER: 68 gmn::TmSharedChunksAllocated = INTEGER: 18 gmn::TmChunksFreed = INTEGER: 0 gmn::TmChunksOversized = INTEGER: 1021 gmn::TmFragmented = INTEGER: 0 gmn::TmTotalLearns = Counter32: 0 gmn::TmActionReject = Counter32: 11 gmn::TmActionSoftReject = Counter32: 0 gmn::TmActionRewriteSubject = Counter32: 0 gmn::TmActionAddHeader = Counter32: 134 gmn::TmActionGreylist = Counter32: 370 gmn::TmActionNoAction = Counter32: 8405 [steffen@gmn ~]#
Fazit
Dieses kurze Beispiel sollte zeigen wie man beliebige Daten via snmpd pass-through zur Verfügung stellt. rspamd diente lediglich als Beispiel, dieses Konzept kann ohne großen Aufwand auf weitere Systeme übertragen werden.