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!). |
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.
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.
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
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.
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.
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.
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).
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 );
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.
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.
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.
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].
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2016/04/Perl
Anbindung der Push-Notifications an ein Unifi-System: https://tris.net/software/unifi-logreader