Bei Eintritt Rapport (Linux-Magazin, April 2016)

Push-Notifications auf sein Handy halten den paranoiden Hobbynetzwerker darüber auf dem Laufenden, welche Clients sich im WLAN zuhause an- und abmelden.

Immer wenn die Lichter an meinem Router wild zu blinken anfangen, ohne dass ich selbst im Netz aktiv bin, kommt bei mir der Verdacht auf, ob nicht einer meiner Nachbarn mein WPA2-Passwort geknackt hat und auf meinem Internet-Account Schabernack treibt. Schließlich wohne ich im zweiten Stock eines Hauses einer Großstadt auf halber höhe eines gewaltigen Hügels, und ich schätze mal, dass ein paar hundert Leute das Wifi-Signal meines WLAN-Routers inspizieren können und ein oder zwei Taugenichtse vielleicht gerne damit herumspielen würden.

Die heute vorgestellten Skripts schnappen sich deswegen in regelmäßigen Abständen die beim WLAN-Router angemeldeten schnurlosen Geräte und schicken mir jedesmal eine kurze Nachricht auf den Handyschirm, wenn sich ein neuer Client in meinem drahtlosen Netzwerk daheim an- oder abmeldet. Dazu musste ich keine neue Handy-App schreiben, vielmehr bietet "Prowl" für's iPhone (und "Notify my Android" für Android-Geräte) ein Web-Interface, über das Anwendungen ihre Nachrichten abschicken können. Die Prowl-Server sorgen anschließend dafür, dass die Events an Endgeräte mit laufenden Prowl-App zu den angemeldeten Usern weitergeleitet werden. Dies funktioniert selbst bei verriegeltem Handy, dann erscheinen die Nachrichten einfach kurz auf der Lockscreen, bevor das Telefon sich wieder schlummern legt (Abbildung 1).

Abbildung 1: So erfährt der Handy-Nutzer, wer sich zuhause ins WLAN eingeklinkt hat.

Abbildung 2: Der Router schiebt die DHCP-Daten auf einen Server, auf dem ein Cronjob den Prowl-Server auf dessen Web-API benachrichtigt, der wiederum eine Push-Notification an ein Handy mit installierter Prowl-App sendet (Bitte ein Diagramm draus machen, danke!).

Unbrauchbares Werkzeug

Wie kommt nun der ASUS RT-66U-Router, auf dem ich sofort nach dem Kauf wie immer reflexartig die freie dd-wrt-Software installiert habe, dazu, die angemeldeten User zu überwachen und bei Änderungen den Web-Service auf dem Prowl-Server anzurufen? Der Prozess, der auf dem Router die dynamischen DHCP-IP-Adressen ausgibt heißt dnsmasq und legt die zur Zeit ausgegebenen DHCP-Leases in der Datei /tmp/dnsmasq.leases ab. Das verwendete Spaltenformat (Listing 1) listet für jede Lease die Gültigkeitsdauer in Sekunden ab, sowie die MAC-Adresse des Geräts, die ihm zugewiesene IP-Adresse und einen Namen, mit dem das Gerät sich selbst beschreibt. Auf einer Linux-Distro wäre es nun relativ einfach, in einer Endlosschleife ankommende oder abgehende Clients zu erfassen und bei Änderungen einen HTTP-Request auf die Prowl-API abzusetzen.

Listing 1: dnsmasq.leases

    1 86400 74:da:42:1b:44:a7 192.168.20.148 raspberrypi *
    2 86400 e8:80:2e:e9:11:a9 192.168.20.130 MikesiPhone e8:80:2e:e9:11:a9
    3 86400 10:68:3a:17:4a:ba 192.168.20.131 android-oba143110ae5ee34 10:68:3a:17:4a:ba
    4 86400 00:51:b6:76:a1:b6 192.168.20.134 Mikes-Macbook 00:51:b6:76:a1:b6

Allerdings bietet die dd-wrt-Distro auf dem Router nur eine sehr eingeschränkte Auswahl an Unix-Tools und die sonst triviale Aufgabe, die Lease-Datei mit Perl zu durchforsten und im Bedarfsfall HTTP-Requests abzufeuern, wird zum Denksporträtsel, etwa so, wie einen Roman zu schreiben und dabei nur die ersten sechs Buchstaben des Alphabets zu verwenden.

Theoretisch könnte man zwar Perl auf dem Router installieren und einige CPAN-Module, aber je mehr die verwendete Konfiguration von einer fabrikneuen dd-wrt-Distribution abweicht, desto fehleranfälliger wäre das Ergebnis bei Upgrades. Ein einfaches Shell-Skript, das nur die aus der Busybox stammenden Tools auf dem Router verwendet, läuft hoffentlich auch in ein paar Jahren noch ohne manuelle Anpassungen und lässt sich relativ problemlos über die dd-wrt-GUI installieren. Diese speichert es im NVRAM des Routers, pflanzt es bei einem Neustart ins neue Dateisystem, und startet seine Endlosschleife.

Listing 2: lease-push.sh

    01 #!/bin/sh
    02 file=/tmp/dnsmasq.leases
    03 var=lease-file
    04 
    05 HOME=/tmp/root
    06 
    07 cd /tmp
    08 uudecode <<'EOT'
    09 begin 664 keyfile
    10 M5V%S(&EC:"!N:6-H="!W96G#GRP@;6%C:'0@;6EC:"!N:6-H="!H96G#GRX@
    11 ...
    12 51\.V=&4N($]D97(@4V-H:6QL97(N
    13 `
    14 end
    15 EOT
    16 
    17 cd /tmp/root/.ssh
    18 uudecode <<'EOT'
    19 begin 664 known_hosts
    20 M3&ER=6T@3&%R=6T@3,.V9F9E;'-T:65L+B!797(@;FEC:'1S(&AA="P@9&5R
    21 ...
    22 >(&AA="!N:6-H="!V:65L+B!$;VYA;&0@5')U;7`N
    23 `
    24 end
    25 EOT
    26 
    27 cd /tmp
    28 while [ "forever" ]
    29 do
    30     sum=`/usr/bin/sha1sum $file | cut -d " " -f1`
    31     stored="$(/usr/sbin/nvram get $var)"
    32 
    33     if [ "$stored" != "$sum" ]
    34     then 
    35 	/usr/bin/scp -i keyfile $file perlsnapshot@somehoster.com:
    36 	/usr/sbin/nvram set $var=$sum
    37     fi
    38 
    39     sleep 30
    40 done

Engelszungen auf dem Router

Das Shellskript in Listing 2 beschränkt sich deswegen darauf, die Datei mit den ausgegebenen IP-Adressen mittels ssh auf einen Account auf einen Server bei meinem Hoster zu spielen, wo ein Cronjob sie regelmäßig auf Abweichungen überprüft und mit Perl und einem von Prowl eingeholten API-Key den Prowl-Server kontaktiert. Selbst diese Aufgabe stellt sich als nicht ganz einfach heraus, denn der scp-Client der auf Busybox basierenden Distro ist nicht voll funktionsfähig und lässt sich nur mit Engelszungen dazu überreden, sich gegenüber dem Server beim Hoster mittels eines privaten Schlüssels zu identifizieren und dann die Datei dorthin zu übertragen.

Gibt man nämlich dem ssh-Prozess auf dd-wrt einen mit ssh-genkey erzeugten private Key, verabschiedet es sich kurz und knapp mit der Meldung, dass irgendwo in den Tiefen seiner Implementierung angeblich ein String zu lang ist. Wer die Meldung googelt, findet heraus, dass die abgespeckte ssh-Version ein spezielles Format für private Keys benötigt. Dieses erzeugt ein Tool namens dropbearkey auf der dd-wrt-Distro auf dem Router, in die man sich mit ssh unter root einloggen kann, nachdem man auf der GUI unter "Services" den ssh-Daemon aktiviert hat. Der Aufruf

    dropbearkey -t rsa -f keyfile

erzeugt dann den private Key in der Datei keyfile. Den zugehörigen public Key zeigt dropbearkey auf der Standardausgabe an. Wer diesen Textstring per Cut-and-Paste in die Datei .ssh/authorized_keys auf dem Server verpflanzt und im Router auf der Kommandozeile mit ssh -i keyfile servername die Verbindung aufbaut, braucht also kein Passwort mehr einzugeben und kann den Prozess deswegen automatisieren.

Backe backe Kuchen

Allerdings überlebt das Dateisystem auf dem Router keinen Reboot, da dieser es im flüchtigen Speicher ablegt und bei einem Neustart aus Informationen aus dem NVRAM zusammenbaut. Deswegen ist es üblich, dass Skripts auf dem Router alle benötigten Informationen in ihren Code einbacken. Die Binärdaten aus der speziellen Private-Key-Datei bändigt Listing 2 deswegen mit der leicht vergreisten Utility uudecode, deren zwar unleserlicher aber genau 61 Zeichen breiter Datensalat perfekt in ein Source-Listing passt. Erzeugt hat ihn vorher der Aufruf

    $ cat keyfile | uuencode -f keyfile

und dessen Ausgabe habe ich von Hand in Listing 2 verfrachtet. Ähnliches gilt für die Datei known_hosts, die die Hostkeys vertrauter Hosts auflistet. Der erste Aufruf von ssh zur Verbindung mit einem neuen Server fragt auf dem Terminal interaktiv nach, ob der Server vertrauenswürdig erscheint. Antwortet der User mit y, legt ssh den Hostkey in ~/.ssh/known_hosts ab. Die uuencode-Daten ab Zeile 19 kamen zustande, in dem ich uuencode auf die so enstandene Datei known_hosts ansetzte und mit der Maus in Listing 2 kopierte.

Bis zu St. Nimmerlein

Die Endlosschleife ab Zeile 28 in Listing 2 prüft nun alle 30 Sekunden auf dem Router, ob die Liste der mit cut ausgeschnittenen MAC-Adressen einen anderen sha1sum-Stempel ergibt als beim vorigen Durchlauf. Damit das Ganze auch nach einem Reboot weiterläuft, speichert Zeile 36 den aktuellen Wert mit nvram set im nichtflüchtigen Speicher des Routers, von wo ihn nvram get in Zeile 31 wieder hervorzaubert, um ihn mit dem aktuellen Wert aus der Lease-Datei zu vergleichen und bei festgestellten Abweichungen die aktuelle Datei auf den Server zu übertragen.

Abbildung 3: Dieses Skript auf dem Router spielt die Datei mit den DHCP-Lease-Einträgen auf den Server.

Damit der Router das Skript nach jedem Reboot ausführt, pflanzt man es auf der Web-UI unter "Administration" und "Scripts" in das mit "Commands" betitelte Textfeld und klickt auf den Knopf "Save At Startup" am unteren Rand des Fensters (Abbildung 3). Geht etwas schief, ist es oft nicht ganz einfach, die Fehlerursache zu finden, da die abgespeckten Kommandos nur sehr liederlich Auskunft geben. Aber wer sich mit ssh auf dem Router einloggt und das Skript von Hand von der Kommandozeile ausführt, kommt der Fehlerursache normalerweise irgendwann näher.

Überbringer

Der Router sorgt also dafür, dass die aktuelle Version der Lease-Datei stets auf einem Linux-Server bereitsteht, von wo sich ein minütlich einsetzender Cronjob ihrer annimmt, Änderungen feststellt, und im Bedarfsfall den Prowl-Web-Service benachrichtigt:

    * * * * * /home/perlsnapshot/lease-notify

Der Prowl-Service verlangt, dass sich Nutzer auf prowlapp.com mit einem Usernamen und Passwort registrieren, die Angabe einer Email-Adresse ist nicht notwendig, hilft aber später aus der Not, falls der User das Passwort vergisst. Ein Account berechtigt zum Empfang von API-Keys, mit denen sich Applikationen, die Events senden wollen, gegenüber dem Webservice ausweisen. Der Prowl-Server leitet die Kurznachrichten dann an alle Instanzen der App weiter, die unter dem Account angemeldet sind. Die App kostet für das iPhone $2.99, die Registrierung auf der Webseite zum Einholen eines API-Keys und das Weitersenden der Push-Notifications ist bei bis zu gesendeten 1000 Events pro Tag kostenlos.

Abbildung 4: Auf prowlapp.com gibt es kostenlose API Keys.

Der einfache Test in Listing 3 veranschaulicht das Verfahren mit dem CPAN-Modul WebService::Prowl, das den Zugriff auf den Webservice schön abstrahiert. Es verlangt einen API-Key, den es auf prowlapp.com unter dem Reiter "API Keys" als Textstring für eingeloggte User gibt (Abbildung 4).

Listing 3: prowl-test

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 
    04 use WebService::Prowl;
    05 my $ws = WebService::Prowl->new( 
    06     apikey => "xxxxxxxxxxxxxxxxxxxxxxx");
    07 
    08 $ws->verify || die $ws->error();
    09 
    10 $ws->add( 
    11     application => "Perl Snapshot",
    12     event       => "Just a test.",
    13     description => "Huzzah, it works!",
    14     url         => "http://linux-magazin.de",
    15 );

Auf Wunsch mit Link

Die Methode verify() in Zeile 8 prüft, ob die Kommunikation mit dem Prowl-Server funktioniert, und die Methode add() in Zeile 10 nimmt die vier Textstrings der übermittelten Nachricht entgegen. Die ersten drei geben den Namen der sendenden Applikation, die Art des Events und einen kurzen Beschreibungstext an. Der vierte Parameter url gibt eine URL an, auf die das Handy springen soll, wenn der User auf den Event tippt und dann sein Einverständnis zum Öffnen des Browsers gibt. In Abbildung 5 kommt die Push-Notification des von Listing 3 abgesetzten Test-Events gerade auf meinem verriegelten iPhone 5 an. Abbildung 6 zeigt den Dialog, der hochkommt, nachdem der User mit dem Finger auf den Event getippt hat.

Abbildung 5: Das iPhone zeigt die eingetroffene Testnachricht auf der Lockscreen an.

Abbildung 6: Die dem Event beiliegende URL wird geöffnet.

Auf der Serverseite läuft das Skript in Listing 4 als Cronjob. Es unterhält einen persistenten Datenspeicher in der Datei leases.dat, mit dem es in Zeile 14 mittels tie den Hash %leases verknüpft. Nach dem Programmstart such lease-notify eine Datei namens dnsmasq.leases, liest diese mit dem CPAN-Modul Path::Tiny ein und wurstelt sich mit lines() in Zeile 20 durch dessen Textzeilen. Die durch Leerzeichen getrennten Felder spaltet split() in Zeile 21 auf in verbleibende Lease-Dauer, MAC-Adresse, vergebene IP-Adresse und den Gerätenamen.

Kommen und Gehen

Im persistenten Hash %leases speichert Listing 4 dann alle gefundenen MAC-Adressen und weist sie den zugehörigen IP-Adressen und Gerätenamen zu. So weiß es später, welche Geräte beim vorigen Durchlauf schon vorhanden waren und welche gerade neu dazugekommen sind. Letztere trägt Zeile 27 dann ebenfalls in den Speicher ein und Zeile 28 schickt mittels der ab Zeile 42 definierten Funktion notify Events ab. Ähnliches gilt für Geräte, die beim vorigen Durchlauf aktiv waren, damit in %leases stehen, aber nun beim aktuellen Durchlauf fehlen. Diese speichert der flüchtige Hash %found und bei einer festgestellten Diskrepanz schickt Zeile 34 eine die Nachricht ab, dass sich ein Teilnehmer verabschiedet hat.

Listing 4: lease-notify

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use lib '/home/perlsnapshot/perl5/lib/perl5';
    04 use Path::Tiny;
    05 use DB_File;
    06 use WebService::Prowl;
    07 use File::Basename;
    08 
    09 my $in_file  = "dnsmasq.leases";
    10 my $db_file  = "leases.dat";
    11 my $API_KEY  = "xxxxxxxxxxxxxxxxxxxxxxx";
    12 my $APP_NAME = "Wifi watch";
    13 
    14 tie my %leases, 'DB_File', $db_file;
    15 
    16 my $file  = path( $in_file );
    17 
    18 my %found = ();
    19 
    20 for my $line ( $file->lines ) {
    21   my( $secs, $mac, $ip, $name ) = 
    22       split " ", $line;
    23 
    24   $found{ $mac } = 1;
    25 
    26   if( !exists $leases{ $mac } ) {
    27     $leases{ $mac } = "$ip $name";
    28     notify( "joined: $leases{ $mac }" );
    29   }
    30 }
    31 
    32 for my $mac ( keys %leases ) {
    33   if( !exists $found{ $mac } ) {
    34     notify( "left: $leases{ $mac }" );
    35     delete $leases{ $mac };
    36   }
    37 }
    38 
    39 untie %leases;
    40 
    41 ###########################################
    42 sub notify {
    43 ###########################################
    44   my( $event ) = @_;
    45 
    46   my $ws = WebService::Prowl->new( 
    47       apikey => $API_KEY );
    48   
    49   $ws->verify || die $ws->error();
    50 
    51   $ws->add( 
    52       application => $APP_NAME,
    53       event       => $event,
    54       description => "Home Wifi clients",
    55       url         => "",
    56   );
    57 }

Die Funktion notify() ab Zeile 42 sieht im Prinzip genau so aus wie das vorher in Listing 2 vorgestellte Testskript, nutzt den am Skriptanfang in $API_KEY abgelegten Prowl-API-Key und füllt nur die Applikationsnamen, die Art des Events ("joined" bzw. "left") und die Beschreibung aus, während sie das URL-Feld leer lässt. Da mein billiger Hosting-Service mir keinen root-Zugriff erlaubt, habe ich die vom Skript benötigten CPAN-Module lokal im Home-Verzeichnis unter perl5 installiert, was cpanm automatisch macht, wenn es feststellt, dass es /usr/lib nicht manipulieren darf. Damit das Skript die dort installierten Module aber findet, muss Zeile 3 mit use lib diesen Pfad explizit definieren.

Noch keine Invasion

Zu meiner großen Beruhigung haben sich während der Testphase des Skripts nur bekannte Geräte angemeldet, aber für den Fall einer Invasion bin ich jetzt gut vorbereitet. Was auffiel, ist, dass sich manche Geräte plötzlich mitten in der Nacht mit dem Wifi verbinden, obwohl sie ausgeschaltet in der Ecke liegen, wie zum Beispiel mein Kindle Paperwhite. Das Skript ließe sich relativ einfach noch verbessern, wenn es wüsste, welche MAC-Adressen zu welchem Gerät gehören, was sich durch einen einfachen Hash in lease-notify bewerkstelligen ließe. Die Kurznachrichten kämen dann mit den auf den persönlichen Haushalt maßgeschneiderten Gerätenamen daher und damit gleich klarer rüber.

Mein Dank geht an meinen Arbeitskollegen Tristan Horn, der nicht nur die Idee hatte, Zu- und Abgänge auf dem Netzwerk auf dem Telefon anzuzeigen, sondern auch noch eine Anwendung geschrieben hat, um das Ganze in ein zugegebenermaßen viel professionalleres Unifi-System einzubinden [2].

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2016/04/Perl

[2]

Anbindung der Push-Notifications an ein Unifi-System: https://tris.net/software/unifi-logreader

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.