Damit ein Anwender schnell über womöglich ungewollte Zu- und Abgänge in seinem Netzwerk informiert wird, speichert ein Perl-Daemon periodisch die Daten von Nmap-Scans und gibt sie über ein eingebautes Webinterface an Nagios weiter.
Der praktische Netzwerkscanner »nmap« dient nicht nur regelbrechenden Akteuren in spannenden Thrillern zum Aufspüren von Einbruchszielen [1], sondern zeigt auch dem Hobby-Admin an, welche Geräte tatsächlich über sein Heimnetzwerk erreichbar sind. Wer in regelmäßigen Abständen einen Nmap-Lauf in seinen Subnetzen startet und die Ausgaben vergleicht, bleibt über neu hinzugekommene Geräte oder Abgänge auf dem Laufenden und baut bösen Überraschungen vor.
Dass »nmap« über eine Option »-oX« verfügt, die ihn zur Ergebnisausgabe im XML-Format veranlasst, erfuhr ich bei der Lektüre einer Nmap-Bedienungsanleitung, die kürzlich als Kindle-Buch erschienen ist [2]. Da ein Nmap-Scan über mehrere Netze schon mal einige Minuten dauern kann, kam mir die Idee, einen Daemon zu bauen, der einmal pro Stunde alle Endknoten findet, die Daten im Speicher behält und sie über einen eingebauten Webserver an anfragende Clients, etwa ein Nagios-Skript, ausgibt.
Schlankes Skript definiert den Suchraum
Das Skript in Listing 1 erledigt dies, allerdings hauptsächlich über das in Zeile 7 hereingezogene und weiter unten besprochene Modul »NmapServer« und dessen Methode »start()« . Vor dem Aufruf legt es im Konstruktor den IP-Bereich des Heimnetzwerks fest, den »nmap« scannen soll – in dem vorliegenden Beispiel sind das die Subnetze 192.168.14.x und 192.168.27.x.
Listing 1
nmap-server
01 #!/usr/local/bin/perl -w 02 ########################################### 03 # nmap-server - Nmap scanning daemon 04 # Mike Schilli, 2014 (m@perlmeister.com) 05 ########################################### 06 use strict; 07 use NmapServer; 08 use App::Daemon qw( daemonize ); 09 10 daemonize(); 11 12 my $nmap = NmapServer->new( 13 scan_range => 14 [ "192.168.14.1/24", 15 "192.168.1.1/24" ] ); 16 17 $nmap->start(); 18 19 my $cv = AnyEvent->condvar(); 20 $cv->recv();
Die Notation »/24« im Listing gibt an, dass die ersten drei Oktette (24 Bit) die Netzwerkmaske des zu scannenden Subnetzes definieren, die hinten angehängte 1 wird »nmap« durch alle Werte von 1 bis 254 ersetzen. Das CPAN-Modul App::Daemon sorgt mit der Methode »daemonize()« dafür, dass das Skript sich mit dem Parameter »start« starten lässt:
./nmap-server start
Alles, was im Skript unterhalb von »daemonize()« steht, läuft bis zum Sankt-Nimmerleins-Tag danach im Hintergrund weiter, selbst wenn das Skript sofort wieder zurückkehrt und die Shell dem User schon den nächsten Prompt zeigt. Auch wenn der Anwender sich ausloggt, läuft der Daemon unbeirrt weiter, weil das Modul App::Daemon hinter den Kulissen dafür gesorgt hat, dass das Skript sein eigener Session Leader ist und so auch nicht mehr von der aufrufenden Shell abhängt.
Um den Daemon wieder herunterzufahren, genügt das Kommando
./nmap-server stop
im selben Verzeichnis, denn das Modul App::Daemon speichert die Prozerss-ID (PID) des Daemon-Prozesses in der Datei »nmap-server.pid« ab.
Was der Daemon so alles treibt, steht in einer Logdatei namens »nmap-server.log« , und mit der Verbose-Option »-v« landen in derselben Datei auch die Debugmeldungen. Wer den Daemon im Vordergrund laufen lassen will, der ruft das Skript genau wie auch den Apache-Server mit der Option »-X« auf.
Ereignisse in Schleifen
Das Modul »NmapServer« ist mit dem Event-Framework »AnyEvent« vom CPAN implementiert, mit dem zwar Multitasking möglich ist, in dem aber zu jedem Zeitpunkt nur ein Thread und normalerweise nur ein Prozess läuft. Der Programmfluss ist asynchron, die verschiedenen Programmteile laufen nur quasi-parallel und generieren und konsumieren Ereignisse, die eine alles steuernde Eventschleife verwaltet.
Die letzten beiden Zeilen in Listing 1 sind deswegen ein »AnyEvent« -Unikum. Sie definieren mit »condvar()« eine Variable, die mit »recv()« auf Events wartet. Da der Variablen jedoch niemand ein Event schickt, wartet das Skript am Ende, verzweigt zur Eventschleife und verarbeitet dort eintreffende Events, bis jemand den Daemon herunterfährt. Fehlten die letzten zwei Zeilen in »nmap-server« , würde sich das Skript nach Zeile 13 verabschieden, wenn die Variable »$nmap« dem Garbage Collector zum Opfer fällt, weil sie das Ende ihres Gültigkeitsbereichs im Programm erreicht hat.
Das Modul »NmapServer.pm« in Listing 2 bringt nun das Kunststück fertig, mit Hilfe eines Timers vom Typ »AnyEvent::Timer« im Stundentakt das Programm »nmap« aufzurufen, dessen Ausgabe in eine mit »File::Temp« angelegte temporäre XML-Datei zu speichern und die Daten anschließend im Json-Format im Speicher abzulegen.
Listing 2
NmapServer.pm
001 ###########################################
002 package NmapServer;
003 ###########################################
004 # Daemon running nmap periodically and
005 # serving JSON data via a web interface
006 # Mike Schilli, 2014 (m@perlmeister.com)
007 ###########################################
008 use strict;
009 use warnings;
010 use Log::Log4perl qw(:easy);
011 use AnyEvent;
012 use AnyEvent::HTTPD;
013 use JSON qw( to_json );
014 use File::Temp qw( tempfile );
015 use XML::Simple;
016
017 ###########################################
018 sub new {
019 ###########################################
020 my( $class, %options ) = @_;
021
022 my( $fh, $tmp_file ) =
023 tempfile( UNLINK => 1 );
024
025 my $self = {
026 xml_file => $tmp_file,
027 fork => undef,
028 json => "",
029 child => undef,
030 scan_range => [],
031 %options,
032 };
033
034 bless $self, $class;
035 }
036
037 ###########################################
038 sub start {
039 ###########################################
040 my( $self ) = @_;
041
042 $self->{ timer } = AnyEvent->timer(
043 after => 0,
044 interval => 3600,
045 cb => sub {
046 if( defined $self->{ fork } ) {
047 DEBUG "nmap already running";
048 return 1;
049 }
050 $self->nmap_spawn();
051 },
052 );
053
054 $self->httpd_spawn();
055 }
056
057 ###########################################
058 sub nmap_spawn {
059 ###########################################
060 my( $self ) = @_;
061
062 $self->{ fork } = fork();
063
064 if( !defined $self->{ fork } ) {
065 LOGDIE "Waaaah, failed to fork!";
066 }
067
068 if( $self->{ fork } ) {
069 # parent
070 $self->{ child } = AnyEvent->child(
071 pid => $self->{ fork },
072 cb => sub {
073 my $data =
074 XMLin( $self->{ xml_file });
075 $self->{ json } =
076 to_json( $data, { pretty => 1 } );
077 $self->{ fork } = undef;
078 } );
079 } else {
080 # child
081 exec "nmap", "-oX",
082 $self->{ xml_file },
083 @{ $self->{ scan_range } },
084 }
085 }
086
087 ###########################################
088 sub httpd_spawn {
089 ###########################################
090 my( $self ) = @_;
091
092 $self->{ httpd } =
093 AnyEvent::HTTPD->new( port => 9090 );
094
095 $self->{ httpd }->reg_cb (
096 '/' => sub {
097 my ($httpd, $req) = @_;
098
099 $req->respond({ content =>
100 ['text/json', $self->{ json } ],
101 });
102 },
103 );
104 }
105
106 1;
Quasi gleichzeitig läuft im Code ein Webserver vom Type »AnyEvent::HTTPD« , der auf Port 9090 lauscht und anfragenden Clients die Json-Daten übermittelt. Abbildung 1 zeigt einen dort andockenden Browser, der die detaillierten Scandaten des letzten Nmap-Laufs im Json-Format anzeigt.
Da das externe Programm »nmap« nicht mit der von »AnyEvent« benutzten Eventschleife kooperiert, sondern den Gesamtbetrieb des Servers aufhalten würde, muss Zeile 62 mit Hilfe der Anweisung »fork()« ein Prozesskind erzeugen, das »AnyEvent« mit der Methode »child()« in Zeile 70 verwaltet. Kommt der Childprozess zum Abschluss, wurde der »nmap« -Lauf also erfolgreich abgeschlossen, springt das Skript den ab Zeile 72 definierten Callback-Code an, verarbeitet das XML und wandelt es in Json um.
Für das Auffrischen des internen Cache muss der Code nicht einmal ein Lock setzen, denn es läuft immer nur ein Thread und eine Zuweisung ist auch dann atomar, wenn es sich um einen riesigen Datenwust handelt. Dank dieser Eventschleife kommt in der Zwischenzeit keine weitere AnyEvent-Task an die Reihe.
Andererseits führt das Prozesskind mit »exec()« in Zeile 81 lediglich das externe Nmap-Programm aus und beendet sich anschließend selbst. Definitionsgemäß kehrt der Prozess nie mehr aus seiner »exec()« -Anweisung zurück, es sei denn, beim Aufruf geht etwas schief.
Eingebauter Webserver
Der Timer ab Zeile 42 startet dank des auf den Wert 0 gesetzten Parameters »after« sofort und nach dem ersten Lauf des Nmap-Scanners wieder im Stundentakt (»interval« steht auf »3600« ). Es ist wichtig, die zurückgereichte Timer-Referenz in einer Instanzvariablen des »NmapServer« -Objekts zu sichern, denn sonst gäbe der Timer sofort den Geist auf, nachdem der Programmfluss die Methode verlassen hat.
Der in der Funktion »http_spawn« ab Zeile 88 definierte und auf Port 9090 lauschende Webserver tickt zusammen mit dem Timer in der Eventschleife und liefert so bei allen Anfragen unter dem Pfad »/« die im Speicher liegenden Json-Daten aus. »AnyEvent« kann dabei eine ganze Reihe von Komponenten wie auf Ports lauschende Server oder ins Netzwerk greifende Clients im selben Skript laufen und auch miteinander kommunizieren lassen.
König Kunde
Möchte nun ein Client die Daten des letzten Nmap-Scans abfragen, holt er sie sich einfach mit einem normalen Webclient ab. Listing 3 verbindet sich mit Port 9090 auf dem lokalen Host, bekommt die Json-Daten, wandelt sie mit dem CPAN-Modul »Json« und dessen Methode »from_json()« in Perl-Datenstrukturen um und iteriert dann über die dort gefundenen Hash- und Array-Einträge.
Listing 3
nmap-client
01 #!/usr/local/bin/perl -w
02 use strict;
03 use JSON qw( from_json );
04 use LWP::UserAgent;
05
06 my $ua = LWP::UserAgent->new();
07 my $resp =
08 $ua->get( "http://localhost:9090" );
09
10 if( $resp->is_error() ) {
11 die "failed: ", $resp->message();
12 }
13
14 my $data =
15 from_json( $resp->decoded_content() );
16
17 for my $host ( @{ $data->{ host } } ) {
18 print "$host->{ address }->{ addr }\n";
19 }
Eine kurze Analyse der Json-Daten zeigt, dass die von Nmap gefundenen Hosts unter einem Hasheintrag mit dem Schlüssel »host« stehen und deren IPv4-Adressen jeweils unterhalb des Knotens »address« in einem Eintrag mit dem Namen »addr« liegen. Listing 3 iteriert über alle gefundenen Einträge und gibt das Ergebnis aus – »./nmap-client« :
192.168.14.1 192.168.14.10 192.168.27.101
Der Scan brachte also insgesamt drei Geräte zum Vorschein, zwei im ersten Subnetz und eines im zweiten.
Als Wächter fungiert nun Nagios
Um das Ganze in ein Monitoringprogramm wie Nagios einzubinden, das Alarm schlägt, falls es mehr als die erwartete Anzahl von Knoten im Netz findet, kommt das Skript in Listing 4 zum Einsatz. Es nutzt das CPAN-Modul Nagios::Clientstatus, das oft wiederholte Aufgaben von Nagios-Skripten abstrahiert, zum Beispiel das Entgegennehmen von Parametern oder das Beenden des Skripts mit einem Exitcode, den Nagios versteht. Das Skript »nagios-check-nmap« (Listing 4) nimmt zwei Parameter entgegen, »–min-hosts« und »–max-hosts« , die die Mindest- beziehungsweise Maximalzahl eines Nmap-Scans vorgeben. Unter- oder überschreitet der Suchlauf die eingestellten Werte, signalisiert das Skript mit »exitvalue(“critical”)« einen Fehler und Nagios schlägt Alarm.
Listing 4
nagios-check-nmap
01 #!/usr/local/bin/perl
02 use strict;
03 use Nagios::Clientstatus;
04
05 my $version = "0.01";
06 my $ncli = Nagios::Clientstatus->new(
07 help_subref =>
08 sub { print "usage: $0\n" },
09 version => $version,
10 mandatory_args =>
11 ['max-hosts', 'min-hosts'],
12 );
13
14 use JSON qw( from_json );
15 use LWP::UserAgent;
16
17 my $ua = LWP::UserAgent->new();
18 my $resp =
19 $ua->get( "http://localhost:9090" );
20
21 if( $resp->is_error() ) {
22 die "failed: ", $resp->message();
23 }
24
25 my $data =
26 from_json( $resp->decoded_content() );
27
28 my $nhosts = scalar @{ $data->{ host } };
29
30 printf "Nmap found: %s\n",
31 join " ",
32 map { $_->{ address }->{ addr } }
33 @{ $data->{ host } };
34
35 my $max_hosts =
36 $ncli->get_given_arg('max-hosts');
37
38 my $min_hosts =
39 $ncli->get_given_arg('min-hosts');
40
41 if( $nhosts > $max_hosts or
42 $nhosts < $min_hosts ) {
43 exit $ncli->exitvalue("critical");
44 }
45
46 exit $ncli->exitvalue("ok");
Um das Nagios-Skript in Listing 4 in die Nagios-Konfiguration einzubinden, sind die in Listing 5 gezeigten Zeilen notwendig. Nach einem Neustart des Nagios-Daemon schnappt sich dieser die neue Konfiguration und ruft das Nagios-Skript in festgesetzten Zeitabständen auf. Nach dem Lauf des »nmap-server« -Daemon bekommt das Skript die Scandaten und meldet, dass alles in Ordnung ist oder dass sich ein neuer Host ins Netzwerk eingeschlichen hat.
Listing 5
nagios.cfg
01 define service{
02 use ez-service
03 service_description Hosts in Nmap Scan
04 check_command nagios-check-nmap!5!5
05 host_name mybox
06 }
07
08 define command{
09 command_name check_nmap
10 command_line $USER1$/nagios-check-nmap --min-hosts $ARG1$ --max-hosts $ARG2$
11 }
Das könnte ein neu gekauftes Gerät oder ein Eindringling sein. Nagios benachrichtigt den User, und der sollte nach dem Rechten sehen und eventuell den in der Konfiguration eingestellten Wert für »–max-hosts« um eins nach oben korrigieren, falls es sich tatsächlich um eine Neuanschaffung handelt.
Infos
- Nmap-Einsatz im Film “The Matrix”: https://www.youtube.com/watch?v=0TJuipCrjZQ
- “Nmap Cookbook: The Fat-free Guide to Network Scanning” (Kindle Edition), Nicholas Marsh: http://www.amazon.com/dp/B005ZK84NU
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2014/11/Perl







