DiLi-Tech

DynDNS-Updater

Hinweis: Dieser Artikel ist schon älter und nicht mehr letzter Stand! Die meisten neueren (DSL-) Router haben eine DynDNS-Funktion eingebaut (z.B. Fritz!Box) und für das Protokollieren der IP-Adress(en)-Historie empfehle ich mein Update.

Die hier angegebenen Dyn-DSN-Anbieter sind auch nicht mehr erste Wahl. dyndns.org ist inzwischen von Oracle übernommen worden und nun kostenpflichtig. Und dtdns.com liegt scheinbar in den "letzen Zügen" und das Updaten funktioniert nur sehr eingeschränkt.

Dieser Artikel kann aber zur Beschreibung grundlegender Web-Programmier-Techniken dienen.

Leider ist es - im Gegensatz zum alten SpeedPort-Router - bei der neuen Fritz!Box nicht mehr so einfach, sich per Skript einzuloggen und Infos zu ziehen... Das Problem ist nicht die sichere Verbindung (durch SSL bzw. https), sondern die JavaScript-"verseuchte" Benutzeroberfläche der Box!


Wenn man aus der Ferne über das Internet den heimischen Server (mit täglich wechselnder IP-Adresse, da über DSL angebunden) erreichen möchte, so bieten sich sog. dynamische DNS-Dienste an. Diese Dienstleister bieten Domains an, die nicht auf eine feste, sondern auch auf eine wechselnde Serveradresse zeigen. Dazu ist es nötig, bei jeder IP-Adressenänderung den Service zu aktualisieren. Üblicherweise werden dazu kleine Clientprogramme auf dem Server gestartet, die diesen Job erledigen. Oder noch besser: der heimische DSL-Router übernimmt diese Aufgabe. Das kann heute eigentlich jeder Router. In der Regel aber nur für eine Adresse und es wird auch kein Protokoll über die verwendeten Adressen geführt.

Aber genau das ist der Punkt: ich möchte mehrere Domains bei unterschiedlichen Dienstleistern aktualisieren und ich möchte ein IP-Adressen-Protokoll haben (man weiß nie, wofür das gut sein kann...). Also war mal wieder der kleine Programmierer gefragt. Was liegt näher, als dafür ein PHP-Script zu schreiben. Die dynDNS-Dienste sind i.d.R. auch über API-Funktionen ansprechbar. Für dtdns.com und dyndns.com beschreibe ich hier eine Lösung. Für letzt genannten Service ist das verbreitete Protokoll "DynDNS v2" von dyndns.org (Beschreibung in englisch) implementiert.

Hier das Skript - mal wieder prozedural statt objektorientiert - bei solch übersichtlichen Aktionen ist objektorientiertes Programmieren m.E. unsinnig! Und es offenbaren sich die technischen Details oftmals viel besser! (Aber manch Zeitgenosse kann gar nicht mehr anders...)

<?php

// dyndns-Updater für dtdns.net und dyndns.com
// (c) DiLi-Soft 5.9.2014


// Definitionen:

$DTDNS_arr=array("xxxx.dtdns.net",           // meine Domains über dtdns.net:
                 "xxxy.dtdns.net");

$DYNDNS_arr=array("xxxx.mine.nu",            // meine Domains über dyndns.com:
                  "xxxx.dyndns.org");

$IP_filename=".\last_IP.txt";                // enthält letzte gesicherte IP

$IP_logfile=".\update_dyndns.log";           // Logfile


# --------------------------------------------------------------------------- #


function get_last_IP() {                     // gesicherte letzte IP aus Datei holen
   global $IP_filename;

   $hd=@fopen($IP_filename,'r');
   if ($hd===FALSE)
      return(FALSE);

   $zeile=fgets($hd);
   $zeile=rtrim($zeile);
   fclose($hd);
return($zeile);                              // IP oder FALSE
}


function put_IP($IP) {                       // aktuelle IP in Datei sichern
   global $IP_filename;

   $hd=@fopen($IP_filename,'w');
   $count=fwrite($hd,$IP);
   fclose($hd);
   return($count);                           // Anz. geschriebener Zeichen
}


function get_myIP() {                        // IP über ext. Dienstleister ermitteln
   $hd=@fopen("http://www.eigene-ip.de","r");
   if ($hd===FALSE)
      return($hd);

   $zeile="";
   while(!feof($hd))
      $zeile.=fgets($hd);
   fclose($hd);

   $tag1='<div class="mapinfo"><b>IP-Adresse:';
   $tag2="</'+'b><br><b>Land:</'+'b>";           // ... <div class="mapinfo"><b>IP-Adresse: xxx.yyy.zzz.aaa</'+'b><br><b>Land:</'+'b> ...

   $zeile=strstr($zeile,$tag1);              // IP isolieren
   $zeile=substr($zeile,strlen($tag1));
   $zeile=substr($zeile,0,strpos($zeile,$tag2));

   $count=substr_count($zeile,".");
   if ($count<>3)                            // 3x "." enthalten?
      $zeile="";

   $zeile=trim($zeile);
return($zeile);                              // IP-Adresse, leer oder FALSE
}


function get_SP500v_IP() {                   // IP im DSL-Modem (hier: Speedport 500V) abfragen
   $sock = fsockopen("192.168.0.1", 80, $errno, $errstr, 10);
   if ($sock==FALSE)
      return(FALSE);

   fwrite($sock, "POST /start.login HTTP/1.1\r\n");                           // mit Passwort einloggen
   fwrite($sock, "Host: 192.168.0.1\r\n");
   fwrite($sock, "Referer: http://192.168.0.1/hcti_start_passwort.htm\r\n");
   fwrite($sock, "Content-Type: application/x-www-form-urlencoded\r\n");
   fwrite($sock, "Content-Length: 7\r\n");
   fwrite($sock, "Connection: close\r\n");
   fwrite($sock, "\r\n");
   fwrite($sock, "P1=0000\r\n");             // = Passwort z.B. 0000

   $headers = "";                            // Header einlesen
   while ($str = trim(fgets($sock)))
      $headers .= "$str\r\n";
   /*
   $body = "";
   while (!feof($sock))                      // Body hier nicht nötig
      $body.= fgets($sock);
   */
   $such1="Set-Cookie: ";                    // zurückgeliefertes Cookie
   $such2="; path=/";
   $cookie=strstr($headers,$such1);          // Cookie isolieren
   $cookie=substr($cookie,strlen($such1));
   $cookie=substr($cookie,0,strpos($cookie,$such2));
   $cookie=trim($cookie);

   fclose($sock);                            // Connection immer wieder schließen!

   if($cookie=="")
      return("");                            // erfolglos: raus!

   $sock = fsockopen("192.168.0.1", 80, $errno, $errstr, 10);
   if ($sock==FALSE)
      return(FALSE);


   fwrite($sock, "GET /hcti_status_netzwerk.htm HTTP/1.1\r\n");               // IP abfragen
   fwrite($sock, "Host: 192.168.0.1\r\n");
   fwrite($sock, "Referer: http://192.168.0.1/hcti_status.htm\r\n");
   fwrite($sock, "Cookie: $cookie\r\n");                                      // Cookie für Sessionverfolgung
   fwrite($sock, "Connection: close\r\n");
   fwrite($sock, "\r\n");

 //$headers = "";
   while ($str = trim(fgets($sock)))         // Header überspringen
      ; //$headers .= "$str\r\n";

   $body = "";
   while (!feof($sock))                      // und Body abholen
      $body.= fgets($sock);

   $such1="var internet_status = 'Verbunden';";
   $such2='<td width="200">Zugeteilte IP-Adresse</td>'."\n\t\t\t\t\t\t<td>";
   $such3="</td>";
   $status=strpos($body,$such1);
   if ($status<>FALSE) {                     // verbunden!
      $IP=strstr($body,$such2);
      $IP=substr($IP,strlen($such2));
      $IP=substr($IP,0,strpos($IP,$such3));
      $IP=trim($IP);                         // hier sollte nun die IP stehen
   } else {
      $IP="";                                // nicht verbunden!
   }
   fclose($sock);

   $sock = fsockopen("192.168.0.1", 80, $errno, $errstr, 10);                 // am Ende vom Modem abmelden !!
   if ($sock==FALSE)
      return($IP);

   fwrite($sock, "GET /logout.cmd HTTP/1.1\r\n");
   fwrite($sock, "Host: 192.168.0.1\r\n");
   fwrite($sock, "Referer: http://192.168.0.1/m_startseite.htm\r\n");
   fwrite($sock, "Cookie: $cookie\r\n");                                      // Cookie für Sessionverfolgung
   fwrite($sock, "Connection: close\r\n");
   fwrite($sock, "\r\n");                    // und Tschüss
   /*
   $headers = "";                            // hier nicht nötig
   while ($str = trim(fgets($sock)))
      $headers .= "$str\r\n";

   $body = "";
   while (!feof($sock))
      $body.= fgets($sock);
   */
   fclose($sock);
return($IP);                                 // mit "" oder IP zurück
}


function update_dtdns($DOMAIN) {             // 1 dtdns-Domain behandeln
   $hd=@fopen("http://www.dtdns.com/api/autodns.cfm?id=$DOMAIN&pw=Passwd&client=myUpdater","r");
   if ($hd===FALSE)                          // ohne "&ip=xx.xx.xx.xx" liefert dtdns die aktuelle IP !!!
      return($hd);

   $zeile=fgets($hd);
   $zeile=rtrim($zeile);
   $len=strlen($zeile);
   if ($len>0)
      if ($zeile[$len-1]==".")
         $zeile=substr($zeile,0,$len-1);     // evtl. Punkt am Ende der IP entfernen
   fclose($hd);
return($zeile);                              // response oder FALSE
}


function update_dtdns_domains($arr) {        // alle dtdns-Domains behandeln
   foreach ($arr as $DOMAIN) {
      $response=update_dtdns($DOMAIN);       // dtdns lässt mehrfaches Auffrischen zu
      if ($response===FALSE)
         $response=$DOMAIN.": ERROR";

      $response=rtrim($response);            // ggf. CRLF entfernen
      $IP=trim(str_replace("Host ".$DOMAIN." now points to ","",$response,$count));
      if ($count<>1)                         // str_replace() fehlerhaft
         $IP=FALSE;                          // damit nichts gemacht wird

      $res[]=array($response,$IP);
   }
return($res);
}


function update_dyndns($DOMAIN,$IP) {        // 1 dyndns-Domain behandeln
   @$sock = fsockopen("members.dyndns.org", 80, $errno, $errstr, 30);
   if (!$sock)
      return(FALSE);

   fwrite($sock, "GET /nic/update?hostname=$DOMAIN&myip=$IP HTTP/1.0\r\n");
   fwrite($sock, "Host: members.dyndns.org\r\n");
   fwrite($sock, "Authorization: Basic ".base64_encode("User:Passwd")."\r\n");
   fwrite($sock, "User-Agent: myUpdater\r\n");
   fwrite($sock, "Accept: */*\r\n");
   fwrite($sock, "\r\n");
   fwrite($sock, "\r\n");

   $headers = "";
   while ($str = trim(fgets($sock, 4096)))
   $headers .= "$str\n";

   $body = "";
   while (!feof($sock))
   $body .= fgets($sock, 4096);

   fclose($sock);
return($body);                               // response oder FALSE
}


function update_dyndns_domains($arr, $IP) {  // alle dyndns-Domains behandeln
   foreach ($arr as $DOMAIN) {
      $response=update_dyndns($DOMAIN,$IP);  // zum Testen auskommentieren !!
      if ($response===FALSE)
         $response="ERROR";

      $res[]="Host $DOMAIN: ".$response;
   }
return($res);
}


function write_log($arr) {                   // Log-File schreiben
   global $IP_logfile;

   $hd=fopen($IP_logfile, "a");
   if ($hd===FALSE)
      return ($dh);

   foreach ($arr as $zeile)
      fwrite($hd,date("d.m.Y H:i:s")." : ".$zeile."\r\n");

   fwrite($hd,"\r\n");
   fclose($hd);
return;
}



# --------------------------- Main() -------------------------------- #

$last_IP=get_last_IP();             // letzte IP aus Datei holen
$IP=$last_IP;                       // erstmal annehmen, dass unverändert


$dtdns_results=update_dtdns_domains($DTDNS_arr); // updaten + akt. IP holen

if ( ($dtdns_results[0][1] <> "") and ($dtdns_results[0][1] <> FALSE) )
   $IP=$dtdns_results[0][1];        // IP übernehmen
else {

   if ( ($dtdns_results[1][1] <> "") and ($dtdns_results[1][1] <> FALSE) )
      $IP=$dtdns_results[1][1];     // oder hier, wenn 1. DOMAIN fehlerhaft war
   else {

      $myIP=get_myIP();             // oder diesen Dienstleister versuchen, wenn auch 2. DOMAIN fehlerhaft
      if ( ($myIP <> "") and ($myIP <> FALSE) )
         $IP=$myIP;                 // nur übernehmen, wenn kein Fehler
      else {

         $ModemIP=get_SP500v_IP();  // als letzter Weg: IP aus DSL-Modem holen
         if ( ($ModemIP <> "") and ($ModemIP <> FALSE) )
            $IP=$ModemIP;
         else {

            if ($ModemIP===FALSE) write_log(array("Fehler beim Ermitteln der aktuellen IP-Adresse !"));
            if ($ModemIP=="")     write_log(array("Keine ADSL-Verbindung !"));
         }
      }
   }
}

if ($IP==$last_IP)                  // IP wurde wg. Fehler nicht gesetzt oder ist unverändert
   exit;                            // raus:
                                    // dyndns darf nicht mehrfach aufgefrischt werden -> "abuse" !!!

                                    // !nur! wenn IP geändert:
$dyndns_results=update_dyndns_domains($DYNDNS_arr,$IP); // updaten

write_log(array("Neue IP: $IP",
                $dtdns_results[0][0],$dtdns_results[1][0],
                $dyndns_results[0],$dyndns_results[1]));

put_IP($IP);                        // letzte gültige (geänderte) IP in Datei sichern

?>

Los geht es im unteren Viertel des Listings. dtdns.com ist sehr pflegeleicht: man kann die API beliebig oft aufrufen und bekommt in der Response auch noch die eigene IP-Adresse geliefert. Sehr bequem. dyndns.com ist ziemlich eigen: die API will die IP-Adresse aktiv mitgeteilt bekommen und das ausschließlich bei einer Änderung. Ansonsten wird man gebannt ("abuse"). Es bietet sich also an, zunächst dtndns zu aktualisieren (regelmäßig) und dann nur bei veränderter IP-Adresse die dyndns-API aufzurufen.

Sollte dyndns ausgefallen sein und somit die eigene IP nicht bekannt sein, so wird ein externer Dienst angesprochen. Sollte auch diese Anfrage scheitern (weil auch diese Seite down ist oder die DSL-Verbindung ausgefallen ist, so bleibt als letzter Weg, im Modem nachzuschauen (das Script ist unverändert ausschließlich für Speedport 500v geeignet). Dazu simuliere ich einen sich einloggenden Benutzer.

Anschließend wird das Protololl aktualisiert (siehe das Beispiel aus dem Protokoll).

Es gibt noch Verbesserungspotential: sollte ein Updaten gescheitert sein (bei dtdns unkritsich, da es regelmäßig wiederholt wird) so wird bei dyndns zurzeit keine Wiederholung durchgeführt. Aktuell läuft das Script alle 10 Minuten.

nach ganz oben

     Kommentieren Sie hier!