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:

  1. -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 😉
  2. -g für „get“: Liefert zur gewünschten OID den passenden Wert. Oder nichts falls es diese OID nicht gibt. Klingt ebenfalls machbar.
  3. -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.