Aus Linux-Magazin 11/2014

Das heimische Netz automatisiert überwachen

© Rainer Plendl, 123RF

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.

Abbildung 1: Die Json-Ausgabe des Nmap-Daemon.

Abbildung 1: Die Json-Ausgabe des Nmap-Daemon.

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

  1. Nmap-Einsatz im Film “The Matrix”: https://www.youtube.com/watch?v=0TJuipCrjZQ
  2. “Nmap Cookbook: The Fat-free Guide to Network Scanning” (Kindle Edition), Nicholas Marsh: http://www.amazon.com/dp/B005ZK84NU
  3. Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2014/11/Perl

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 erscheinenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mailto:mschilli@perlmeister.com beantwortet er gerne Fragen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben