Damit Clients blitzschnell auf die Daten des letzten Nmap-Scans des lokalen Netzwerks zugreifen können, speichert ein Perl-Dämon sie zwischen und gibt sie über ein eingebautes Webinterface heraus.
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 über alle
bekannten Subnetze 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 neu erschienenen
firlefanzlosen Nmap-Bedienungsanleitung,
die kürzlich als Kindle-Buch erschienen ist ([3]). Da ein nmap
-Scan
über mehrere Netze schon mal einige Minuten dauern kann, kam mir die Idee,
einen Dämon zu bauen, der einmal pro Stunde alle Endknoten findet, die
Daten im Speicher behält, und über einen eingebauten Webserver an anfragende
Clients wie zum Beispiel ein Nagios-Skript verzögerungsfrei ausgibt.
Das Skript in Listing 1 erledigt dies, allerdings hauptsächlich über das
in Zeile 3 hereingezogene und weiter unten besprochene
Modul NmapServer
und dessen Methode
start()
. Vordem Aufruf legt es im Konstruktor den von nmap
abzudeckenden IP-Bereich des Heimnetzwerks fest, im vorliegenden Beispiel
sind das die Subnetze 192.168.14.x
und 192.168.27.x
, die Notation
/24
gibt an, dass die ersten drei Oktette (24bit) die Maske des
zu scannenden Subnetzes
definieren, die hinten angehängte 1 wird nmap
also 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
Sanktnimmerleinstag danach im Hintergrund weiter, auch wenn das Skript
sofort wieder zurückkehrt und die Shell dem User schon den nächsten Prompt
zeigt. Auch wenn letzterer sich ausloggt, läuft der Dämon weiter, weil
App::Daemon
hinter den Kulissen dafür gesorgt hat, dass er sein
eigener Session-Leader ist und nicht mehr von der aufrufenden Shell
abhängt.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use NmapServer; 04 use App::Daemon qw( daemonize ); 05 06 daemonize(); 07 08 my $nmap = NmapServer->new( 09 scan_range => 10 [ "192.168.0.1/24", 11 "192.168.0.31/24" ] ); 12 13 $nmap->start(); 14 15 my $cv = AnyEvent->condvar(); 16 $cv->recv();
Um den Dämon wieder herunterzufahren, genügt das Kommando
$ ./nmap-server stop
im gleichen Verzeichnis, denn App::Daemon
speichert die pid des
Dämon-Prozesses in der Datei nmap-server.pid
ab. Was der Dämon treibt,
steht in der Logdatei nmap-server.log
, und mit der Verbose-Option
-v
landen dort auch Debug-Meldungen. Wer den Dämon im Vordergrund laufen
lassen will, ruft das Skript genau
wie den Apache-Server mit der Option -X
auf.
Das Modul NmapServer
ist mit dem Event-Framework AnyEvent
vom CPAN
implementiert, mit dem zwar Multitasking möglich ist, aber in dem 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 einen Event schickt,
wartet das Skript am Ende, verzweigt zur Eventschleife, und verarbeitet
dort eintreffende Events bis jemand den Dämon 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 abzulegen und die Daten anschließend
im JSON-Format im Speicher abzulegen. 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 Scan-Daten des
letzten nmap-Laufs im JSON-Format anzeigt.
Abbildung 1: Die JSON-Ausgabe des Nmap-Daemons. |
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 Child-Prozess 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. Um den internen Cache aufzufrischen, muss der Code nicht
einmal ein Lock setzen, denn es läuft immer nur ein Thread und eine
Zuweisung ist auch dann atomar, falls es sich um einen riesigen Datenwust
handelt. Dank Eventschleife kommt in der Zwischenzeit keine weitere
AnyEvent-Task an die Reihe.
Das Prozesskind führt andererseits mit exec()
in Zeile
81 lediglich das externe nmap
-Programm aus und beendet sich
anschließend. Definitionsgemäß kehrt der Prozess nie mehr aus seiner
exec()
-Anweisung zurück, es sei denn, beim Aufruf geht etwas schief.
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 Timerreferenz 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 bei jeglichen
Anfragen unter dem Pfad "/" immer die im Speicher bereitliegenden JSON-Daten
aus. AnyEvent kann so eine ganze Reihe von Komponenten wie auf Ports
lauschende Server, ins Netzwerk greifende Clients oder Timer in ein und
demselben Skript laufen lassen. Das macht Integrationstest besonders einfach,
denn man kann ohne Probleme einen Server und einen Client im gleichen
Skript laufen und miteinander kommunizieren lassen, ohne dass externe
Prozesse gestartet und später wieder gestoppt werden müssten.
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;
Möchte nun ein Client die Daten des letzten Nmap-Scans abfragen, holt
er sie einfach mit einem normalen Webclient ab. Listing 3 verbindet sich
mit Port 9090 auf dem lokalen Host, bekommt die JSON-Daten, wandelt diese
mittels des CPAN-Moduls JSON
und dessen Methode from_json()
in
Perl-Datenstrukturen um und iteriert dann über die dort gefundenen
Hash- und Arrayeinträge. 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-Adresse jeweils unterhalb des Knotens address
in einem Eintrag mit dem Namen addr
liegen. Listing 3 iteriert über
alle gefundenen Einträge und gibt sich 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.
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 }
Um nun das Ganze in ein Monitorprogramm wie Nagios einzubinden, das
Alarm schlägt, falls mehr als die erwartete Anzahl von Knoten im Netz
gefunden wurde, kommt das Skript in Listing 4 zum Einsatz. Es nutzt
das CPAN-Modul Nagios::Clientstatus, das oft wiederholte Aufgaben von
Nagios-Skripts abstrahiert, wie zum Beispiel das Entgegennehmen von
Parametern oder das Beenden des Skripts mit einem Exit Code, den Nagios
versteht. Das Skript nagios-check-nmap
in Listing 4 nimmt zwei
Parameter entgegen, --min-hosts
und --max-hosts
, die die Mindest-
bzw. Maximalzahl eines Nmap-Scans vorgeben. Unter- bzw. überschreitet
der Suchlauf die eingestellten Werte, signalisiert das Skript
mit exitvalue("critical")
einen Fehler und der Nagios-Dämon schlägt
Alarm.
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");
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 } 12
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-Dämons schnappt sich dieser die neue Konfiguration und
ruft das Nagios-Skript in festgesetzten Zeitabständen auf. Nach dem Lauf
des nmap-server
-Dämons bekommt das Skript die Scan-Daten und meldet
entweder, dass alles in Ordnung ist oder sich ein neuer Host ins Netzwerk
eingeschlichen hat. Das könnte entweder ein neu gekauftes Gerät oder ein
Eindringling sein. Es benachrichtigt Nagios den User, und dieser
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.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2014/11/Perl
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