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
- HOW-TO: be your own DDNS provider: Gute Beschreibung über den Prozess, jedoch ist das key-Verfahren etwas kompliziert gemacht worden.
- Set up your own Dynamic DNS: Beschreibt das Key-Verfahren besser als „HOW-TO: be your own DDNS provider„.
- Dynamic DNS – Replacing dyndns with Bind: Weitere Beschreibung
- nsupdate: Painless Dynamic DNS: Beschreibt die Benutzung von nsupdate
- HOWTO – Delegate a Sub-domain (a.k.a. subzone): Beschreibt die Erstellung von Sub-Zonen
- DNS BIND Zone Transfers and Updates: Die Optionen von bind’s: update-policy übersichtlich dargestellt
- bind9.net Chapter 4. Advanced DNS Features: Administrations-Handbuch zum Thema
- O’Reilly „DNS and BIND 10.2. DNS Dynamic Update: Aus dem O’Reilly Buch
- redhat Using the rndc Utility: Beschreibung des rndc-tools von redhat.
- kb.isc.org How do I share a dynamic zone between multiple views?: Lösung, falls man ein System mittels internen und externen Zonen („Split-DNS“) hat und die dynamische Zone mit diesen teilen möchte
- ddclient – Usage: Beschrieb über die Optionen von ddclient.
- DynDNS-API: Beschreibung der DynDNS-API
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
Vielen Dank für den Hinweis, habe das mittlerweile so aktualisiert. 🙂
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?
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.