Wer von außen durch die eigene Firewall ins LAN gelangen will, braucht entweder ein geheimes Loch oder einen kooperativen Agenten auf der Innenseite. Ein Jabber-Client nimmt von innen aktiv Kontakt zum öffentlichen Jabber-Server auf und wartet auf Instruktionen seiner Internet-Buddies in Form von Instant Messages.
Um vom Internet aus Aktionen auf dem lokalen Netzwerk auszulösen, könnte man freilich ein Loch in die hoffentlich vorhandene Firewall bohren und einen lokalen Webserver ins Internet stellen. Dynamisch vergebene IP-Adressen des Internetproviders verfolgen Services wie dyndns.org und erlauben beinah-statischen Zugriff.
Einfacher geht's heute mit einem Agenten oder ``bot'' (wohl von ``Robot''):
Ein auf der Innenseite der Firewall laufender Messaging-Client hängt
im öffentlichen Jabber-Messaging-Netzwerk und versteht
Kommandos per Textnachricht. Er akzeptiert nur Befehle von Clients
aus seiner Buddy-List und erlaubt nur vier Aktionen: Die Last auf
dem Agenten-Rechner zu bestimmen, die öffentliche
Adresse des Routers abzufragen (Kommando: ip
),
und das Licht im Schlafzimmer in meiner Wohnung in San Francisco
ein- und auszuschalten (lamp on|off
).
Abbildung 1: Der "Bot" hinter der Firewall führt Befehle aus, die ein Jabber-Client vom Internet sendet. |
Das Skript agent.pl
nutzt Log::Log4perl
und führt in der
Datei /tmp/agent.log
Buch über die ausgeführten Transaktionen.
Zeile 24 erzeugt ein neues Net::Jabber::Client
-Objekt, das
einen Instant-Message-Client implementiert.
Bevor agent.pl
in die Haupt-Eventschleife in Zeile 61 eintritt, müssen
ab Zeile 26 noch einige Callbacks für eintretende Ereignisse definiert
werden. Der onauth
-Handler ab Zeile 48 kommt an die Reihe, wenn
der Client sich erfolgreich mit dem in Zeile
17 definierten Nutzernamen beim Server angemeldet hat. Der Handler
fragt dann mittels RosterGet()
die Buddy-List ab und speichert sie im
globalen Hash %ROSTER
.
Die anschließend abgesetzte Methode Presence()
schickt allen Clients
im Roster eine presence
-Nachricht, die diesen anzeigt, dass agent.pl
Online ist. Ab diesem Zeitpunkt zeigt ein gaim
-Client mit
mikes-agent-sender
als eingeloggtem Benutzer mikes-agent-receiver
als aktiven Client in seiner Buddy-List an (Abbildung 2).
Abbildung 2: Der "Bot" erscheint in der Buddy-List des Senders. |
Der message
-Callback ab Zeile 28 kommt zum Zug, wenn der Kommandeur eine
Nachricht an agent.pl
schickt.
Jeder beliebige Client im Jabber-Netzwerk
könnte dies tun. Deshalb prüft Zeile 34, ob es sich beim Gesprächspartner
auch um jemanden bekannten handelt. Im vorliegenden Fall darf es nur
mikes-agent-sender
sein, da die Buddy-List des Clients nur ihn enthält
(siehe Abschnitt Installation).
Andere Anfrager lässt die Funktion abblitzen
und kehrt in Zeile 37 zur Hauptschleife
zurück.
Das im Message-Text geschickte Steuerungskommando kramt in Zeile 41 die
getBody()
-Methode hervor und gibt sie an die ab Zeile 71 definierte
run_cmd
-Funktion weiter.
001 #!/usr/bin/perl 002 ########################################### 003 # agent - Jabber agent behind firewall 004 # Mike Schilli, 2004 (m@perlmeister.com) 005 ########################################### 006 use warnings; 007 use strict; 008 009 use Net::Jabber qw(Client); 010 use Log::Log4perl qw(:easy); 011 use LWP::Simple; 012 013 Log::Log4perl->easy_init( 014 { level => $DEBUG, 015 file => '>>/tmp/agent.log' }); 016 017 my $JABBER_USER = 'mikes-agent-receiver'; 018 my $JABBER_PASSWD = "*****"; 019 my $JABBER_SERVER = "jabber.org"; 020 my $JABBER_PORT = 5222; 021 022 our %ROSTER; 023 024 my $c = Net::Jabber::Client->new(); 025 026 $c->SetCallBacks( 027 028 message => sub { 029 my $msg = $_[1]; 030 031 my $sender = $msg->GetFrom(); 032 033 DEBUG "Message '", $msg->GetBody(), 034 "' from $sender"; 035 036 # Remove /GAIM resource 037 $sender =~ s#/\w+$##; 038 039 if(! exists 040 $ROSTER{$sender}) { 041 INFO "Denied ($sender not in roster ", join('#', keys %ROSTER), ")"; 042 return; 043 } 044 045 DEBUG "Running ", $msg->GetBody(); 046 my $rep = run_cmd($msg->GetBody()); 047 chomp $rep; 048 DEBUG "Result: ", $rep; 049 050 $c->Send($msg->Reply(body => $rep)); 051 }, 052 053 onauth => sub { 054 DEBUG "Auth"; 055 %ROSTER = $c->RosterGet(); 056 $c->PresenceSend(); 057 }, 058 059 presence => sub { 060 # Ignore all subscription requests 061 }, 062 ); 063 064 DEBUG "Connecting ..."; 065 066 $c->Execute( 067 hostname => $JABBER_SERVER, 068 username => $JABBER_USER, 069 password => $JABBER_PASSWD, 070 resource => 'Script', 071 ); 072 073 $c->Disconnect(); 074 075 ########################################### 076 sub run_cmd { 077 ########################################### 078 my($cmd) = @_; 079 080 # Find out external IP 081 if($cmd eq "ip") { 082 return LWP::Simple::get( 083 "http://perlmeister.com/cgi/whatsmyip" 084 ); 085 } 086 087 # Print Load 088 if($cmd eq "load") { 089 return `/usr/bin/uptime`; 090 } 091 092 # Switch bedroom light on/off 093 if($cmd =~ /^lamp\s+(on|off)$/) { 094 my $rc = system("/usr/bin/lamp $1"); 095 return $rc == 0 ? "ok" : 096 "not ok ($rc)"; 097 } 098 099 return "Unknown Command"; 100 }
Execute()
ab Zeile 61 nimmt Verbindung mit dem Jabber-Server
auf jabber.org
auf und meldet mikes-agent-receiver
dort an. Die Haupt-Eventschleife
erholt sich auch bei temporär abreißender Verbindung und sollte
nie enden. Falls sie nach zuvielen Fehlern doch abbricht, räumt
Zeile 68 auf und das Programm endet.
Die IP-Adresse meines
Routers findet der Agent heraus, indem er einen Web-Request auf
dei öffentlich zugängliche URL
http://perlmeister.com/cgi/whatsmyip
abfeuert. Dort hängt nur
ein einfaches Skript, das einfach die Client-Adresse des Requests
zurückgibt:
print "Content-Type: text/html\n\n"; print $ENV{REMOTE_ADDR}, "\n";
agent.pl
zieht hierzu LWP::Simple
heran, dessen get
-Funktion
holt in Zeile 77 den Inhalt der Webseite ein, falls die empfangene
Textnachricht ip
war.
Ähnliches gilt für die aktuelle Rechnerlast: Zeile 84 ruft
uptime
auf und gibt das Ergebnis an Zeile 41 zurück, wo
es mit chomp
zurechtgestutzt, geloggt und schließlich in Zeile 45
in einen Message-Body verpackt und per Send
an den Chat-Partner
zurückgeschickt wird.
Abbildung 3: |
Doch wie schaltet der auf einem Linux-Rechner laufende Agent das Schlafzimmerlicht an?
Abbildung 3 zeigt das System im Zusammenspiel. In den USA gibt es (selbstverständlich nur für das dortige elektrische System) die X10-Technologie, die Signale über die elektrischen Leitungen im Haus umher schickt und auch über eine serielle (oder auch USB)-Schnittstelle mit dem Rechner kommunizieren kann. Jede Kontrolleinheit (Abbildung 4) verfügt über einen House-Code (A-K) und einen Unit-Code (1-9), den die Steuereinheit (z. B. Abbildung 5) selektieren muss, damit auch das richtige Licht (und nicht das des Nachbarn) angeht. Das ganze ist nicht teuer: Ein vielteiliges Einsteigerkit mit allerhand Schnickschnack und Fernbedienung kostet auf [4] zwischen 50 und 100 Dollar.
Abbildung 4: Die X10-Kontrolleinheit wartet auf Signale und schaltet den Strom an oder ab. |
Abbildung 5: Die X10-Steuereinheit mit seriellem Anschluss schickt Signale vom Rechner über das Stromnetz an die Kontrolleinheit. |
Abbildung 6: Die Schlafzimmerlampe wurde übers Internet angeschaltet. |
Listing lamp.pl
zeigt das kurze Skript, das die Codes über den
seriellen Port aussendet und damit die Lampe steuert. Es nutzt lediglich
Device::ParallelPort
und ControlX10::CM11
vom CPAN,
um zunächst die richtige Einheit
zu mit House/Unit-Code adressieren und dann nochmals den House-Code
gefolgt von J (ein) oder K (aus) zu schicken. Als serieller Port
wurde in Zeile 28 /dev/ttyS0
gewählt, da der kleine weiße Kasten
aus Abbildung 6 im ersten seriellen Port des Rechners steckt.
Ich gebe zu, mein
Rechner ist nicht der neueste, aber Linux ist genügsam.
01 #!/usr/bin/perl 02 ########################################### 03 # lamp -- Switch lamp on and off via x10 04 # Mike Schilli, 2004 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 use Device::SerialPort; 10 use ControlX10::CM11; 11 12 my $UNIT_CODE = "K"; 13 my $HOUSE_CODE = "9"; 14 15 my %cmds = ( 16 "on" => "J", 17 "off" => "K", 18 ); 19 20 die "usage: $0 [on|off]" if @ARGV != 1 21 or $ARGV[0] !~ /^(on|off)$/; 22 23 my $onoff = $1; 24 25 die "You must be root" if $> != 0; 26 27 my $serial = Device::SerialPort->new( 28 '/dev/ttyS0', undef); 29 $serial->baudrate(4800); 30 31 # Adress unit 32 ControlX10::CM11::send($serial, 33 $UNIT_CODE . $HOUSE_CODE); 34 35 # Send command 36 ControlX10::CM11::send($serial, 37 $UNIT_CODE . $cmds{$onoff});
lamp.pl
greift auf den seriellen Port des Computers zu und muss
deswegen als root
laufen. Zeile 25 prüft die effektive User-Id
und bricht das Programm ab, falls sie nicht die root
gehörenden
Nummer 0 entspricht.
Da der Jabber-Client unter einem
minderpriviligierten Nutzer läuft, definiert Listing
lamp.c
einen C-Wrapper, der einfach mit
gcc -o lamp lamp.c
übersetzt wird. Mit gesetztem setuid-Bit kann ein 'normaler' Nutzer
dann das Perl-Skript /usr/bin/lamp.pl
als root
aufrufen:
$ ls -l /usr/bin/lamp* -rwsr-xr-x 1 root root 11548 Oct 2 08:48 lamp -rwxr-xr-x 1 root root 742 Oct 2 08:45 lamp.pl
In dieser Konfiguration kann nur root
das Skript lamp.pl
verändern,
aber ein normaler Nutzer lamp.pl
unter der effektiven ID root
ablaufen lassen.
1 2 main(int argc, char **argv) { 3 execv("/usr/bin/lamp.pl", argv); 4 }
Aber zurück zum Agenten:
Damit nicht jeder dahergelaufene Jabber-Client Befehle senden kann, akzeptiert
agent.pl
nur Nachrichten von Leuten auf seinem Roster.
Bei einer eingehenden
Nachricht prüft Zeile 34, ob der Sender auf der erlaubten Liste steht und
verweigert sonst den Zugriff.
Der ab Zeile 54 definierte Handler für Presence
-Requests ist leer und
ignoriert alle Anfragen von Clients, die den Agenten auf ihre Buddy-Liste
setzen wollen. Das wäre noch nicht so schlimm, doch der
von Net::Jabber
bereitgestellte Default-Handler
akzeptiert solche Anfragen nicht nur, sondern setzt anfragende Clients
sofort auf seine eigene Buddy-Liste.
Ein leerer presence
-Handler verhindert dies.
Bei der Installation des Agenten muss dessen Buddy-Liste gesetzt werden,
am besten mit einem gaim
-Client ([5]), der unter Angabe des Passworts
einen neuen Account mikes-agent-receiver
anlegt, sich dort
einloggt und den ebenfalls frischangelegten mikes-agent-sender
auf die Liste setzt (Abbildung 7).
Sind beide Accounts online, kommt für mikes-agent-sender
der Dialog in
Abbildung 8 hoch, auf dem ``Authorize'' zu klicken ist, damit der Server den
Vorgang erlaubt. mikes-agent-sender
bekommt anschließend noch das
Angebot, mikes-agent-receiver
in seine Buddy-List aufzunehmen
(Abbildung 9), was dieser praktischerweise tut, damit der Sender nur auf
den angezeigten Namen klicken muss, um ein Kommando an den Agenten zu senden.
Nach dem anschließenden Logout sollte der Account mikes-agent-receiver
nur noch vom Skript agent.pl
und nicht mehr von anderen Clients benutzt
werden, damit diese nicht versehentlich seine Buddy-Liste umkrempeln, die
als Authorisierungs-Mechanismus dient.
Abbildung 7: |
Abbildung 8: |
Abbildung 9: |
Nach dem Start von agent.pl
sollte mikes-agent-receiver
in der
Buddy-Liste von mikes-agent-sender
erscheinen (Abbildung 2).
Die Logdatei /tmp/agent.log
protokolliert die einzelnen Schritte.
Beim Probieren bitte behutsam vorgehen, jeder Implementierungsfehler könnte ein Loch in die Firewall reißen -- also Vorsicht!
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |