Eigener DynamicDNS (ddns) Dienst

Kürzlich ärgerte ich mich wieder einmal über die monatliche Reaktivierung, die man bei seinem kostenlosen DyDNS-Hoster wie z.B. dyndns.com oder noip.com vornehmen muss.

Deshalb recherchierte ich ein bisschen über Lösungen und kam dann darauf, dass, wenn man bereits einen eigenen Nameserver und Webserver mit statischer IP hat, man daraus ganz einfach seinen eigenen „DynDns-Dienst“ machen kann! 😉
Hier die Beschreibung wie das geht.

Hinweis: Pfade
Dieses Tutorial geht von einer Konfiguration wie unter DNS-Server und WWW-Server beschrieben aus.
Weicht deine Konfiguration an, musst du die Pfade der Config-Files ggf. anpassen.

DNS-Server Konfiguration

Als erstes geht es darum den DNS-Server zu konfigurieren.

Da man dynamische Updates meist nicht in seiner primären DNS-Zone haben möchte, erstellen wir extra dafür eine neue Subzone: ddns.example.com:

$ORIGIN .
$TTL 3600       ; 1 hour
ddns.example.com    IN SOA  ns1.example.com. dnsadmin.example.com. (
                                1          ; serial
                                3600       ; refresh (1 hour)
                                900        ; retry (15 minutes)
                                604800     ; expire (1 week)
                                300        ; minimum (5 minutes)
                                )
                        NS      ns1.example.com.
                        NS      ns2.example.com.
                        A       93.184.216.34

Zu beachten ist hier, dass wir die Zone diesmal nicht in /etc/named/, sondern in /var/named/dynamic/ erstellen, da diese Datei von bind zur Laufzeit selbstständig verändert wird.

Anstelle von: 93.184.216.34 (IP-Adresse von example.com); kann hier die IP, des Webservers eingetragen werden; dann können Updates gleich über http://ddns.example.com/ stattfinden.

Nun konfigurieren wir die Zone noch in der zones.conf des Nameservers:

zone "ddns.example.com"            IN { type master; file "/var/named/dynamic/ddns.example.com"; allow-query { any; }; update-policy { grant ddns.example.com. subdomain ddns.example.com. A TXT; }; };

Neu bei dieser Definition ist nur der geänderte Pfad in /var/named/ (anstatt /etc/named/) und die update-policy. Diese besagt, dass nur Hosts mit dem Key: ddns.example.com Einträge auf dem Nameserver unterhalb der Subdomain: *.ddns.example.com ändern dürfen und auch nur A und TXT records.

Nun generieren wir den Schlüssel (key) mit dem der Webserver später die Einträge in der Zone: ddns.example.com ändern darf:

ddns-confgen -r /dev/urandom -q -a hmac-md5 -k ddns.example.com -z ddns.example.com. > /etc/named/ddns.example.com.key
chown -v root:named /etc/named/ddns.example.com.key
chmod -v 640 /etc/named/ddns.example.com.key

Der key wird nun in /etc/named/zones.conf eingebunden:

include "/etc/named/keys/ddns.key";

Das war’s nun auf der Nameserver-Seite. Mittels einen restart laden wir die Konfiguration nun:

/etc/init.d/named restart
Achtung

Es ist normalerweise nicht vorgesehen dynamische Zonen manuell zu bearbeiten.
Falls trotzdem mal eine manuelle Änderung in der Zone ddns.example.com gemacht werden muss, ist unbedingt vorher das Kommando „rndc freeze“ und danach „rndc thaw“ anzuwenden:

rndc freeze
vim /var/named/dynamic/ddns.example.com
rndc thaw

Weitere Informationen finden sich im redhat Manual, Kapitel: Using the rndc Utility

Hinweis bei views

Hat man mehrere „views“, z.B. weil man die Zonen unterscheiden will zwischen internen und externen clients (AKA: „Split-DNS“), muss man beim rndc reload zusätzlich die entsprechende view angeben.
Also z.B.: rndc freeze ddns.example.com IN external-zone (in diesen Beispiel heisst die view: „external-zone“)

WWW-Server Konfiguration

Nun kommt der Webserver-Teil. Wir erstellen ein PHP-Script welches via dem system-tool nsupdate die Zone ddns.example.com dynamisch updated.

Als erstes erstellen wir in der apache Konfiguration einen neuen VirtualHost für ddns.example.com und kopieren das Key-File von Nameserver dahin:

scp nameserver:/etc/named/ddns.example.com.key /srv/www/example.com/ddns/ddns.example.com.key

Nun machen wir erst einen Manuellen-Test mit nsupdate:

[apache@webserver]$ /usr/bin/nsupdate -k /srv/www/example.com/ddns/ddns.key
> server ns1.example.com
> zone ddns.example.com.
> update delete test.ddns.example.com.
> update add test.ddns.example.com. 60 A 127.0.0.1
> update add test.ddns.example.com. 60 TXT 'updated on $date'
> send
> quit

Nun müsste es auf dem Nameserver ein file: /var/named/dynamic/ddns.example.com.jnl geben, welches (in binärer) Form die Änderungen enthält.

Hinweis

Bind schreibt die Änderungen im .jnl-File nur jede Stunde (oder bei manuellem restart von bind) in das File /var/named/dynamic/ddns.example.com.

Hat das geklappt, gilt es lediglich noch ein php-script zu erstellen, welches das macht:

<?php
$keyfile = "/srv/www/example.com/ddns/ddns.example.com.key";
$nameserver = "ns1.example.com";
$zone = "ddns.example.com";
$ip = $_SERVER['REMOTE_ADDR'];
$current_date = date(DATE_RFC822);
$subdomain = "foo";
$update_domain = "$subdomain.$zone";
 
$output = shell_exec("
/usr/bin/nsupdate -k $keyfile <<EOF
server $nameserver
zone $zone.
update delete $update_domain.
update add $update_domain. 60 A $ip
update add $update_domain. 60 TXT \"updated on $current_date\"
send
EOF
";
 
echo "updated zone: $zone, subdomain: $update_domain with ip: $ip";
?>
Achtung: Schutz einbauen

Damit nicht jedermann über die Webseite die DNS-Einträge modifizieren kann, muss unbedingt ein Passwortschutz (z.B. über apache .htaccess) eingebaut werden!

Dieses Script kann man nun beliebig erweitern, bzw. auf die Anforderungen deines DDNS-Update Clients (z.B. ddclient) anpassen.

DynDNS-Kompatibles Server-Script

Damit man nun jedoch standardisierte DDNS-update Clients Verwenden kann, sollte man ein script machen welches das Protokoll des wohl bekanntesten DDNS-Anbieters DynDNS implementiert.

Zuerst müssen wir mittels einer Apache .htaccess Datei die URL nach deren Standard „umschreiben“. Ausserdem kann so auch gleich die Benutzerverwaltung implementiert werden:

AuthType Basic
AuthName "Restricted to authorized DDNS updaters"
AuthUserFile /srv/www/example.com/ddns/.htpasswd
require valid-user
 
RewriteEngine on
RewriteRule ^nic/update(.*)$  /dyndns.php$1

Danach legen wir noch mit:

htpasswd -c /srv/www/example.com/ddns/.htpasswd testuser

mindestens einen User an.

Nun kommt das eigentliche Script. Dieses holt sich den HTTP-AUTH Benutzernamen und macht daraus eine Subdomain in der Form: <benutzername>.<subdomain>.ddns.example.com:

&lt;?php
/******************************************************************************
* dyndns.php
* CREATED BY: Steven Varco
*
* Script for updatinf DDNS-Entries
*
* HISTORY:
*==============================================================================
* 23.08.2013    Steven Varco    - Created Script
*******************************************************************************/
 
 
// ========================== VARIABLES =======================================
ini_set('expose_php', 'off');
 
$keyfile = "/var/www/ddns/private/ddns.key";
$nameserver = "ns1.example.com";
$zone = "ddns.example.com";
$myip = $_SERVER['REMOTE_ADDR'];
$username = strtolower($_SERVER['PHP_AUTH_USER']);
$current_date = date(DATE_RFC822);
 
# DynDNS-Protocol variables
$system = $_REQUEST['system'];
$hostname = $_REQUEST['hostname'];
$myip = $_REQUEST['myip'];
 
$subdomain = str_replace(".$username.$zone", "", $hostname);
 
// -------------------------- MESSAGES ----------------------------------------
$language="en";
 
$message['de']['nochg']="&lt;h1&gt;IP-Adresse gleich - keine Aenderung&lt;/h1&gt;";
$message['de']['good']="&lt;h1&gt;IP-Adresse unterschiedlich - Aenderung erfolgreich&lt;/h1&gt;";
$message['de']['911']="&lt;h1&gt;DynDNS-Fehler - IP-Adresse konnte nicht gespeichert werden&lt;/h1&gt;";
$message['de']['badauth']="&lt;h1&gt;Authentifizierungs fehlgeschlagen&lt;/h1&gt;";
 
$message['en']['nochg']="&lt;h1&gt;IP-Address equal - No change&lt;/h1&gt;";
$message['en']['good']="&lt;h1&gt;IP-Address different - Change successful&lt;/h1&gt;";
$message['en']['911']="&lt;h1&gt;DynDNS-Error - IP-Address couldn't be saved&lt;/h1&gt;";
$message['en']['badauth']="&lt;h1&gt;Authentication failed&lt;/h1&gt;";
 
 
// ========================== INITIALISATION / CHECKS =========================
if (isset($myip) &amp;&amp; preg_match_all("/(\d{1,3}\.){3}\d{1,3}/", $myip, $matches))
{
  // using defined ip: $myip
}
else
{
  //using incoming ip: $_SERVER['REMOTE_ADDR']
  $myip = $_SERVER['REMOTE_ADDR'];
}
 
if (isset($subdomain) &amp;&amp; preg_match_all("/^[a-zA-Z]+$/", $subdomain, $matches))
{
  // using defined subdomain: $subdomain
}
else
{
  // subdomain not set
  header ("X-UpdateCode: X");
  header("Content-Type: text/plain");
  header("Accept-Ranges: none");
  echo "notfqdn";
}
 
 
if (! isset($username) &amp;&amp; preg_match_all("/[\w\d-_\.\*]+/", $username, $matches))
{
  header("Content-Type: text/plain");
  header("Accept-Ranges: none");
  echo "badauth";
  die ("&lt;em&gt;FATAL:&lt;/em&gt; No username received from HTTP-AUTH!");
}
 
$update_domain = "$subdomain.$username.$zone";
$old_ip = gethostbyname("$update_domain");
 
 
// ========================== UPDATE ==========================================
if ( isset($subdomain) &amp;&amp; isset($myip) &amp;&amp; isset($username))
{
  if ( preg_match_all("/(\d{1,3}\.){3}\d{1,3}/", $myip, $matches) )
  {
    unset($matches);
    if ( preg_match_all("/[\w\d-_\.\*]+/", $subdomain, $matches) )
    {
      unset($matches);
      if ($myip != $old_ip)
      {
 
        $dns_update_excec = "
/usr/bin/nsupdate -k $keyfile &lt;&lt;EOF
server $nameserver
zone $zone.
update delete $update_domain.
update add $update_domain. 60 A $myip
update add $update_domain. 60 TXT \"updated on $current_date\"
send
EOF
";
 
        shell_exec($dns_update_excec);
        header ("X-User-Status: provider");
        header("X-UpdateCode: n");
        header("Content-Type: text/plain");
        header("Accept-Ranges: none");
        echo "good $myip";
      }
      else
      {
        // IP-Adress ($myip) is still the same as your old ip ($old_ip). Omitting update.
        shell_exec($dns_update_excec);
        header ("X-User-Status: provider");
        header("X-UpdateCode: n");
        header("Content-Type: text/plain");
        header("Accept-Ranges: none");
        echo "nochg $myip";
      }
    }
    else
    {
      // Malicious subdomain-Format
      header ("X-User-Status: provider");
      header("Content-Type: text/plain");
      header("Accept-Ranges: none");
      echo "nohost";
    }
  }
  else
  {
    // Malicious IP-Format
    header ("X-User-Status: provider");
    header("Content-Type: text/plain");
    header("Accept-Ranges: none");
    echo "nohost";
  }
}
else
{
  // Either subdomain, ip or username is not set
  header("Content-Type: text/plain");
  header("Accept-Ranges: none");
  echo "911";
 
}
?&gt;
php-fcgi und HTTP-Auth

Wenn man, wie oben beschrieben den Usernamen, mit welchem sich der Benutzer per HTTP (apache .htaccess) anmeldet standardmässig als subdomain benutzt, z.B.:

$username = $_SERVER['PHP_AUTH_USER'];
$update_domain = "$subdomain.$username.$zone";

Dann muss man beim php-fcgi Modul jedoch noch zusätzlich folgendes Eintragen:

# Path HTTP-Authentication to php-fcgi
FcgidPassHeader Authorization

Danke an: Mario auf php.net für diesen Tipp

Benutzung

Um nun die IP für den hostnamen somehost.testuser.ddns.example.com zu setzten muss man lediglich die folgende Adresse aufrufen:

http://ddns.example.com/nic/update?hostname=somehost.testuser.ddns.example.com

Dabei ist somehost der Name des Hostnamen und testuser der Benutzername.

Oder eben alternativ die Daten in einen DynDNS Client eintragen.

Related Links

Published by

Steven Varco

Steven ist ein Redhat RHCE-Zertifizierter Linux-Crack und ist seit über 15 Jahren sowohl beruflich wie auch privat auf Linux spezialisiert. In seinem Keller steht ein Server Rack mit diversen ESX und Linux Servern.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.