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.

[stextbox id=“note“ caption=“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.[/stextbox]

Inhalt

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

[stextbox id=“warning“ caption=“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[/stextbox]

[stextbox id=“note“ caption=“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“)[/stextbox]

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.

[stextbox id=“note“ caption=“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.[/stextbox]

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"; ?>

[stextbox id=“warning“ caption=“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![/stextbox]

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:

<?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']="<h1>IP-Adresse gleich - keine Aenderung</h1>";
$message['de']['good']="<h1>IP-Adresse unterschiedlich - Aenderung erfolgreich</h1>";
$message['de']['911']="<h1>DynDNS-Fehler - IP-Adresse konnte nicht gespeichert werden</h1>";
$message['de']['badauth']="<h1>Authentifizierungs fehlgeschlagen</h1>";
 
$message['en']['nochg']="<h1>IP-Address equal - No change</h1>";
$message['en']['good']="<h1>IP-Address different - Change successful</h1>";
$message['en']['911']="<h1>DynDNS-Error - IP-Address couldn't be saved</h1>";
$message['en']['badauth']="<h1>Authentication failed</h1>";
 
 
// ========================== INITIALISATION / CHECKS =========================
if (isset($myip) && 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) && 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) && preg_match_all("/[\w\d\-_\.\*]+/", $username, $matches))
{
  header("Content-Type: text/plain");
  header("Accept-Ranges: none");
  echo "badauth";
  die ("<em>FATAL:</em> No username received from HTTP-AUTH!");
}
 
$update_domain = "$subdomain.$username.$zone";
$old_ip = gethostbyname("$update_domain");
 
 
// ========================== UPDATE ==========================================
if ( isset($subdomain) && isset($myip) && 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 <<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";
}
?>

[stextbox id=“tip“ caption=“php-fpm 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 in der .htaccess Datei noch zusätzlich folgendes Eintragen:

# Pass authorisation headers
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

[/stextbox]

[stextbox id=“tip“ caption=“php-fcgi und HTTP-Auth (LEGACY!)“]php-fcgi sollte eigentlich nicht mehr verwendet- und stattdessen auf PHP-FPM gesetzt werden.

Wird trotzdem noch php-fcgi verwendet, dann muss man beim php-fcgi Modul noch zusätzlich folgendes Eintragen:

FcgidPassHeader Authorization

Danke an: Mario auf php.net für diesen Tipp[/stextbox]

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- und Kubernetes CKA Zertifizierter Linux-Crack und ist seit über 20 Jahren sowohl beruflich wie auch privat auf Linux spezialisiert. In seinem Keller steht ein Server Rack mit diversen ESX und Linux Servern.

5 thoughts on “Eigener DynamicDNS (ddns) Dienst”

  1. Mit PHP-Version 7.3 wird das ganze in den else-Zweig „Malicious subdomain-Format geschickt.
    Die Ursache hierfür ist, dass das Minuszeichen in preg_match_all „escaped“ werden muss.
    Richtig ist also:

    if ( preg_match_all("/[\w\d\-_\.\*]+/", $subdomain, $matches) )

    LG

    Rüdiger

  2. Was brauch ich denn da für eine Serverart, um das zu bewerkstelligen?
    Reicht da ein normales Webhosting-Paket mit CLI-Zugriff? Oder muss es zwingend ein v-/root-Server sein?

    1. Für den Updater reicht ein einfacher Webserver. Für den DNS Teil brauchst du einen Webhoster bei dem du deine DNS Zone über ein Script updaten kannst (also nicht nur über das Web-UI des Hosters, sondern auch per Script), dann sollte es klappen.

Schreibe einen Kommentar zu Steven Varco Antworten abbrechen

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