Dynamische DNS-Eintraege sind praktisch. Vorausgesetzt, dass mein Provider es zulaesst, kann ich immer an meine Geraete herankommen. Ohne, dass ich die jeweiligen IP-Adressen auswendig kennnen muss. Lange Zeit habe ich dazu wie so viele dyndns.com benutzt. Dann haben sie angefangen, die kostenlose Nutzung immer weiter zu beschneiden. Und schliesslich haben sie es endlich geschafft -- ich hab keine Lust mehr. Dann bau ich mir das eben selber! So schwer kann das doch nicht sein.

Ausgangslage

Ich moechte also meinen eigenen dynamischen DNS-Server betreiben. Gratulation! Und wie macht man das nun am geschicktesten?
Ich habe die Domain ad001.de. Ich kann mir einen Subdomain delegieren lassen -- zum Beispiel dynamic.ad001.de. Nun muss ich nur noch einen DNS-Server fuer diese delegierte Zone aufsetzen. Und ihn irgendwie dazu bringen, name.dynamic.ad001.de auf Zuruf einer IP-Adresse zuzuordnen. Also einen A-Record (um ins Zonefile-DNS-Sprech zu fallen) zu setzen. Wenn mir danach ist, auch einen MX-Record.

Organisatorische und Softwarevoraussetzungen

Was braucht es also? Schauen wir auf die Zutatenliste:

Geschenkte Zusatzfunktionalitaet

Einen kleinen Moment mal. Ich werde am Ende meiner Bastelei einen Dienst haben, der sich regelmaessig durch Abrufe einer vorgegebenen URL auf einem definierten Server zu erkennen geben wird. Das bedeutet im Umkehrschluss natuerlich auch, dass dieser Server mitbekommt, welcher Rechner wann wo und von welcher IP aus online ist. Dazu habe ich noch die Moeglichkeit implementiert, einen kurzen Hinweistext zu uebergeben. Wer will kann hier uebergeben, ob das System gerade hochgefahren wurde, eine Nutzeranmeldung stattfand oder ob es ein rein zeitgesteuerter Heartbeat sein soll.
Damit tut mein Dienst natuerlich nichts anderes, was nicht auch "Facebook", "Google" und "Spiegel Online" tun -- ein Bewegungsprofil anlegen. Also eigentlich langweilig. Ausser in einem Fall: Diebstahl. Oder "Abhandenkommen", wenn der Verteidiger spricht. Denn dann ist es hoechst interessant, von welcher IP aus sich mein Geraet das naechste Mal beim Server meldet. Und dann gibt es zwei Interessen, die ich verfolgen kann:

Das funktioniert natuerlich nur fuer die notwendige Bedingung eines hinreichend dummen Diebes. Der ein geklautes Geraet direkt an sein (mit dem Internet verbundenes) Netzwerk anschliesst.
Ein Ausbau des Dienstes zu einem "Selbstzerstoerungsmechanismus" sei dem fortgeschrittenen Leser als Uebungsaufgabe ueberlassen. Im Anschluss an die Vernichtung seines Home-Verzeichnisses erwarte ich einen zehnseitigen Aufsatz zum Titel "Meine groesste Dummheit der letzten 32 Stunden" an ad001@uni-rostock.de.

Und wer nun irrtiert ist oder sich gar beschweren will, dass da quasi ein gesamtes Bewegungsprofil entsteht, der moege sich einmal ueberlegen, wo dieses Bewegungsprofil ansonsten noch herumliegt. Ich hab da oben schon ein paar Verdaechtige genannt. Und da waren noch gar keine Seiten mit pornographischem Inhalt dabei...

nsd einrichten

Ich habe mich als Nameserver fuer den nsd entschieden, da ich mit ihm schon aus einem anderen Kontext Erfahrung habe und ihn als den netten kleinen Bruder des Ueberfliegers BIND in Erinnerung habe. Als kurzerhand installiert und ein minimales Config-File angelegt:

[ad001@ad001 ~]$ cat /usr/local/etc/nsd/nsd.conf
server:
        # don't answer VERSION.BIND and VERSION.SERVER CHAOS class queries
        hide-version: yes
        # listen only on IPv4 connections
        ip4-only: yes
        # listen only on IPv6 connections
        ip6-only: no
        username: bind

zone:
        name: "dynamic.ad001.de"
        zonefile: "forward/dynamic.ad001.de"
Damit weiss der nsd, dass sich alles fuer die DNS-Zone "dynamic.ad001.de" im genannten Zonefile abspielen wird. Und dieses Zonefile sieht im Grundstock so aus:
[ad001@ad001 ~]$ cat /usr/local/etc/nsd/forward/dynamic.ad001.de
$TTL 10
@       IN      SOA     ad001.is.some.where. hostmaster.dynamic.ad001.de. (

                        2011110503      ; Serial
                        10              ; Refresh
                        10              ; Retry
                        10              ; Expire
                        600 )           ; Negative Cache TTL
                IN      NS      ad001.is.some.where.
                IN      A       1.2.3.4
update          IN      A       1.2.3.4


blubb           IN      A       127.0.0.1
blubb           IN      MX      10      127.0.0.1.

Das war's auch schon. Zugegeben, man muss ein paar Ersetzungen machen. Das war's dann aber schon, nun gilt weltweit (hier auf einer Maschine des RZ der Uni Rostock):
ad001@triton:~> host -a blubb.dynamic.ad001.de
Trying "blubb.dynamic.ad001.de"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5853
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;blubb.dynamic.ad001.de.            IN      ANY

;; ANSWER SECTION:
blubb.dynamic.ad001.de.     10      IN      MX      10 127.0.0.1.
blubb.dynamic.ad001.de.     10      IN      A       127.0.0.1

Received 77 bytes from 139.30.8.7#53 in 51 ms
ad001@triton:~>

Das war soweit ja schonmal angenehm einfach. Nun muss sich der Spass nur noch auf Zuruf aendern lassen.

Aktualisierung

Hier kommt nun wieder ein wenig geskripteter Frickelkram. Ich habe mich fuer folgendes Design entschieden:

Diese Zweiteilung ergibt sich fuer mich daraus, dass ich vermeiden moechte, dass ein wiederholtes aufrufen der update.php dazu fuehrt, dass auch der Nameserver jedes Mal behelligt wird. Einmal pro Minute ist fuer DNS-Verhaeltnisse schon sehr schnell... bedenkend, dass DNS auch gerne einmal mit Update-Zeiten von 24 Stunden arbeitet.

Zunaechst nun also die update.php. Sie wird von einem Apache mit PHP-Modul ganz normal ueber Port 80 angeboten. Dabei koennen folgende Parameter uebergeben werden: update.php?host=hostname[&trace=1 [¬e=notice] ]. Dabei ist host der Hostname, der hinzugefuegt oder aktualisiert werden soll. Er wird vom Skript von saemtlichen nicht-alphanumerischen Zeichen bereinigt und auf 30 Zeichen begrenzt. trace ist eine optionale Angabe -- wenn hier eine "1" uebergeben wird, wird dieser Aufruf ins tracefile eingetragen und hinterlaesst Fusstappsen (jenseits derer im Webserverlog ;). Mit note kann schliesslich noch eine Notiz hinzugefueht werde, die auf 50 Zeichen begrenzt ist (hier ist mir relativ egal, was da hineingeschieben wird, nur bitte keine Anf"uhrugszeichen oder Semikola. Die IP-Adresse des einzutragenden Hosts hingegen wird direkt aus dem Aufruf ermittelt, um Spielkindern das Vergnuegen zu nehmen, beliebige Aliase zu definieren. Ausserdem ist ein Parameter weniger im Aufruf immer auch ein Parameter weniger, den ein Aktualisierungsclient kennen muss...

<?php
        $ip = getenv('REMOTE_ADDR');
        $host = $_REQUEST['host'];
        $trace = ($_REQUEST['trace'] == '1');
        $host = preg_replace('/[^0-9a-zA-Z]/', '', $host);
        $host = substr($host, 0, 30);

        if ($host == "update") {
                print "sorry. this hostname is reserved for this service :)\nplease try another one.\n";
                exit(0);
        }


        $file = fopen("../data/$host", "w");
        fwrite($file, $ip);
        fclose($file);

        print "assigning $ip to $host.\n";

        if ($trace) {
                if (! is_dir('traces')) {
                        mkdir('traces');
                        touch('traces/index.html');
                }
                $rev = gethostbyaddr($ip);
                $file = fopen("traces/$host", "a");
                date_default_timezone_set("Europe/Berlin");
                $date = date("Ymd;His");

                $note = $_REQUEST['note'];
                $note = preg_replace('/[;"\']/', '',  $note);
                $note = substr($note, 0, 50);
                fwrite($file, "$date;$ip;$rev;$note\n");
                fclose($file);
                print "trace noted at $date with reverse entry $rev and notice \"$note\".\n";
        }

?>

wie zu sehen, werden die Daten einfach erstmal in Textdateien gekippt. Klar, man koennte auch eine Datenbank benutzen, wenn man es denn fein, saeuberlich und komplett ordentlich machen wollte -- das hier soll aber einfach nur funktionieren und zeigen, dass man wirklich so einfach einen dynamischen DNS-Dienst auf die Beine bekommt...

Der zweite Teil ist dann das Aktualisierungsskript compare.pl. Es nimmt die gewonnenen Adressdaten und prueft, ob es Veraenderungen zum bestehenden Stand gibt. Hier habe ich zu meinem schweizer Allzweckmesser PERL gegriffen. Der Code ist wahrscheinlich nicht schoen, aber er tut genau das, was er soll. Was macht der Code? Zunaechst ermittelt er, welche Eintraege neu sind oder sich geaendert haben. Im Anschluss wird das Zonefile bearbeitet (nun ja, gelesen und modifiziert ausgegeben), so dass es mit den neuen Eintraege uebereinstimmt.
Ein wichtiges Detail: Der Timestamp. Weit verbreitet ist ja yyyymmddrr. Da ich mir vorstellen kann, an einem Tag auf mehr als 99 Aenderungen zu kommen (man weiss ja nie...), habe ich mich fuer yymmddHHMM (mit HHMM als Stunde und Minute) entschieden. Wenn das Skript nicht mehr als einmal pro Minute eine Aenderung durchfuehrt, ist das sicher. Wenn es hingegen zweimal mit dem gleichen Timestamp liefe waere die letzte Aenderung bis zur naechsten Aenderung in einer anderen Minuten potentiell unsichtbar.

use strict;

my @infiles = </usr/local/www/vhosts/de.ad001.dynamic.update/data/*>;
my $OLDPATH = "/usr/local/www/vhosts/de.ad001.dynamic.update/data.current";

my $filename;

my @changes;
my %hosts;


foreach $filename (@infiles) {
        my $hostname = $filename;
        $hostname =~ s!.*/(.*)$!$1!;
        my $new_ip = `cat $filename`;
        my $old_ip = `cat $OLDPATH/$hostname 2>/dev/null`;
        if ($new_ip ne $old_ip) {
                push @changes, $hostname;
                $hosts{$hostname} = $new_ip;
                $hosts{$hostname . ".done"} = "0";
                `echo -n $new_ip > $OLDPATH/$hostname`
        }
}
open(f, '<', "/usr/local/etc/nsd/forward/dynamic.ad001.de");
my @zonefile = <f>;
close(f);


for (@zonefile) {
        my $ln = $_;
        chomp($ln);
        if ($ln =~ m/^[^0-9]*([0-9]*).*Serial.*$/) {
                my $oldser = $1;
                my $newser = `date +%y%m%d%H%M`;
                chomp($newser);
                while (!($newser > $oldser)) {
                        $newser++;
                }
                $ln = "\t\t\t$newser\t; Serial";
        }
        for (@changes) {
                my $hostname = $_;
                if ($ln =~ m/$hostname.*/) {

                        my $ip = $hosts{$hostname};
                        $hosts{$hostname . ".done"} = "1";
                        $ln =~ s/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/$ip/g;
                        last;
                }
        }
        print $ln . "\n";
}
for (@changes) {
        my $host = $_;
        if ($hosts{$host . ".done"} ne "1") {
                print "$host\t\tIN\tA\t" . $hosts{$host} . "\n";
                print "$host\t\tIN\tMX\t10\t" . $hosts{$host} . ".\n";
        }
}

Wie man im Code sieht, wird mit der uebergebenen IP-Adress sowohl ein A- als auch ein MX-Record gefuettert. Warum der MX-Record? Weil ich kann :)

...und schliesslich die update, die von einem Cronjob minuetlich aufgerufen wird und den Nameserver aktuell haelt:

#! /bin/sh

export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/home/ad001/bin

TEMPFILE=/tmp/zone.$$$

cd /usr/local/www/vhosts/de.ad001.dyn.update/bin/

/usr/local/bin/perl compare.pl > $TEMPFILE

mv $TEMPFILE /usr/local/etc/nsd/forward/dyn.ad001.de

/usr/local/sbin/nsdc rebuild >/dev/null 2>/dev/null
/usr/local/sbin/nsdc reload >/dev/null 2>/dev/null
Hier gaebe es natuerlich noch deutliches Optimierungspotential -- der Nameserver wird momentan bei jedem Aufruf aktualisiert, egal ob es noetig ist oder nicht.

Aktualisierungsclient Mac OS / FreeBSD

Da ein einfacher HTTP-Aufruf ausreicht, reicht es voellig, curl zu benutzen. Das ist klein und auf zimlich vielen Systemen vorhanden. Erfreulicherweise sogar auch auf einem Mac OS X 10.4 (Tiger). Hier also meine Aktualisierungszeile:

#! /bin/sh

/usr/local/bin/curl "http://update.dynamic.ad001.de/update.php?host=`/bin/hostname`&trace=1¬e=heartbeat" >/dev/null 2>/dev/null || echo -n
Das ist das Skript in der FreeBSD-Geschmacksrichtung. Fuer Mac OS sieht es wie folgt aus:
#! /bin/sh

/usr/bin/curl "http://update.dynamic.ad001.de/update.php?host=`/bin/hostname`&trace=1¬e=heartbeat" >/dev/null 2>/dev/null || echo -n
...und unterscheidet sich folglich nur im Pfad fuer das Hilfsprograemmchen curl.
Als naechstes muss noch dafuer gesorgt werden, dass die Aktualisierungen irgendwie regelmaessig durchgefuehrt werden. Dazu gibt es den allseits beliebten cron-Daemon. Also kurz einen Zeile der Form
*/5     *       *       *       *       /Users/ad001/heartbeat
mit absolutem Pfad zum Skript in die crontab eingetragen -- voila. Und schon meldet er sich alle fuenf Minuten zur Aktualisierung :)

Aktualisierungsclient Windows (getestet mit Windows 2000)

Unter Windows... wird das etwas kitzeliger. Grund ist das Fehlen der vielen kleinen Helferlein, die man aus der Unixwelt einfach zu haben gewoehnt ist. Wie skriptet man unter Windows? Klar, ich koennte mit mit Batchfiles im cmd-Stil umherplagen (...ich schaue dezent zu http://hypftier.de... und wieder weg...). Dann kommt noch die Tatsache dazu, dass ich mit einem Windows 2000 arbeite. Damit fliegt Powershell auch gleich wieder aus der Liste der Optionen raus. Das von Windows mitgebrachte ftp ist im Gegensatz zu dem von den meisten Unix-Derivaten mitgebrachten nicht zum Ausfuehren von HTTP-Requests in der Lage. Und ansonsten bleibt nur die Frage, welches kleine Zusatzprogramm von allen unertraeglichen das am wenigsten unertraegliche ist.
Nein, das kann es doch nicht sein. Das ist es auch nicht. Denn es gab auch vor Powershell schon recht brauchbare Moeglichkeiten, Windows zu skripten. Das ist heute nur ein wenig in Vergessenheit geraten. Es geht um VBScript. Zugegeben, seit der ILOVEYOU-Sache (http://de.wikipedia.org/wiki/ILOVEYOU) hat VBScript auch einen etwas schweren Stand.
Trotzdem, genau das, was ich brauche. Ein kurzes Googeln fuehrte mich zu http://stackoverflow.com/questions/204759/http-get-in-vbs, wo ich den Grundstock fuer das folgende Aktualisierungsskript serviert bekomme:

Dim o
Set o = CreateObject("MSXML2.XMLHTTP")
o.open "GET", "http://update.dynamic.ad001.de/update.php?host=wpcdn&trace=1¬e=heartbeat", False
o.send
Das nun in eine kleine Datei mit der Endung .vbs und schon kann ich es wie ein ganz normales Programm nutzen. Damit es nun noch zur Ausfuehrung gelangt (der Teil ist unter -- gerade den fruehen Windows-Versionen -- etwas kitzeliger) nehme man sich die Geplanten Tasks, die meist zimlich unbeobachtet in der Systemsteuerung herumliegen. Damit kann man das Skript wahlweise an den Systemstart, Logon oder einen feste Zeit pinnen. Mit neueren Windows-Varianten vielleicht auch regelmaessig ausfuehren (Windows 2000 kann das noch nicht).

Sicherheitsbedenken (und Faulheit)

Natuerlich habe ich keinen Gedanken an die Sicherheit verschwendet. Naja, okee, vielleicht doch den ein oder anderen, der aber in dieser prototypischen Umsetzung nicht zum tragen gekommen ist.
Die Eingabe wurde streng validiert. Wenn ich mich nicht geirrt habe, sollte das Zonefile vom Web-Interface aus nicht zu zerschiessen sein.
Es findet keine Authentifikation statt. Das ist momentan wirklich so. Jeder koennte die update.php aufrufen und die wildesten Eintraege hinterlassen. Und ueberschreiben. Und die Traces koennte auch jeder auslesen, der will, denn auch die liegen einfach so auf dem Webserver umher. Wenn man nur den Hostnamen kennt... -- um es kurz zu machen: Ja, ich weiss, dass das so ist. Ja, das koennte man alles noch aendern, wenn man das ganze wirklich professionell oder gar produktiv nutzen wollen wuerde. Hier war es aber mehr das Proof-of-concept, dass im Mittelpunkt stand, also waren diese Aspekte sekundaer.
Die Authentifikation gelaenge wohl am einfachsten, indem man den Webserver hierzu hinzuzieht: HTTP-Auth, das ganze noch ein SSL gepackt. Die Frage ist dann natuerlich, wie die Aktualisierungsskripte damit umgehen...
Um es nochmal klar zu sagen: Das hier ist ein Proof-Of-Concept. Kein Code fuer produktive Systeme!.

Dank

Danken moechte ich dem lando (http://lando.cc), der sich selten zu schade ist, meine wuseligen Ideen mit Hardware und Infrastruktur zu untermauern und Realitaet werden zu lassen.

Stichworte:


Impressum