Eigenen DynDNS Dienst unter Linux aufbauen

Diese Anleitung zeigt auf, wie man auf einem Linux-Server mit Web- und DNS-Server seinen eigenen DynDNS Dienst aufziehen kann. Die Schritt-für-Schritt Anleitung zeigt alle relvanten Einstellungen auf, für den produktiven Betrieb wäre ein zusätzlicher Authentifizierungsprozess dann natürlich dringend empfohlen.

Für den Betrieb des DynDNS-Dienstes benötigt man folgendes:

  • Linux-Server mit Root-Zugriffsrechten
  • Web- und DNS Server
  • eine beliebige Domain, welche auf dem Server verwaltet wird

Die Anleitung basiert auf einem RedHat EL Clone (AlmaLinux, CentOS, ..) lässt sich aber grundsätzlich auf andere Webserver adaptieren. Dadurch können sich Anpassungen an Pfaden oder Programmen (z.B. Paketmanager) ergeben.

DNS-Server vorbereiten

Wenn der Server schon als DNS-Server in Betrieb können einzelne Punkte übersprungen werden und relevant ist dann primär die DynDNS-Subdomain mit Update Key.

Als DNS-Server entscheide ich mit für bind-chroot, welcher sich bequem über den Paket-Manager installieren lässt:

dnf install bind-chroot bind-utils
systemctl enable named-chroot.service
/bin/systemctl start named-chroot.service

Der DNS-Server sollte nun laufen und kann von der Kommandozeile aus beliebige DNS-Namen abrufen, das kann mit

dig www.google.ch +short @127.0.0.1

getestet werden.

In dem Beispiel gehen wir von einer Domain mysampledomain.ch aus, für die wir eine Subdomain dyndns.mysampledomain.ch erfassen, unter der dann beliebige Hosts erfasst werden können (test.dyndns.mysampledomain.ch). Hier muss jeweils die gewünschte Domain verwendet werden.

Als erstes müssen wir einen Update-Key erstellen:

dnssec-keygen -a HMAC-MD5 -b 256 -n HOST mysampledomain.ch.

Das generiert dann eine .key und .private Datei. Die Key-Datei sieht in etwa so aus:

mysampledomain.ch. IN KEY 512 3 157 BxUCvbBoN/BzQjGEbJpWZ9BaZArgBeB/XOkuzkTIPMA=

Den Key von "BxUc" bis und mit dem "=" am Ende der Datei benötigen wir noch mehrmals.

Nun kann die Datei /var/named/chroot/etc/named.conf editiert und dort die neue Domain und Subdomain erfasst werden. Ebenfalls muss der DNS-Server noch auf allen Ports hören.

options {
  listen-on port 53 { any; };
  listen-on-v6 port 53 { any; };
  allow-query { any; };
  allow-recursion { 127.0.0.1/32; ::1; };
}
//recursion yes;

key "ddns-key.dyndns.mysampledomain.ch" {
  algorithm hmac-sha256;
  secret "BxUCvbBoN/BzQjGEbJpWZ9BaZArgBeB/XOkuzkTIPMA=";
};

zone "mysampledomain.ch"{
 type master;
 file "pri/db.mysampledomain.ch";
};

zone "dyndns.mysampledomain.ch"{
 type master;
 file "pri/db.dyndns.mysampledomain.ch";
 update-policy {
   grant ddns-key.dyndns.mysampledomain.ch subdomain dyndns.mysampledomain.ch. A;
 };
};

Dann müssen wir noch das Verzeichnis /var/named/chroot/var/named/pri erstellen (muss für named-Benutzer les- und schreibbar sein) und in diesem Verzeichnis die zwei Zonen-Files. Als DNS-Server ist bei mir eine virtuelle Maschine mit der IP 172.29.1.140 im Einsatz, auch dieser Wert ist variabel.

/var/named/chroot/var/named/pri/db.mysampledomain.ch

$TTL 1800
@ IN SOA ns1.mysampledomain.ch. hostmaster.mysampledomain.ch. (
         2022031901 ; serial
               1800 ; refresh (30 minutes)
               1800 ; retry (30 minutes)
              86400 ; expire (24 hours)
               1800 ; minimum (30 minutes)
         )

@        IN NS ns1.mysampledomain.ch.
@        IN NS ns2.mysampledomain.ch.

dyndns   IN NS ns1.mysampledomain.ch.
dyndns   IN NS ns2.mysampledomain.ch.

ns1      IN A  172.29.1.140
ns2      IN A  172.29.1.140

/var/named/chroot/var/named/pri/db.dyndns.mysampledomain.ch

$ORIGIN .
$TTL 1 ; 1 second
dyndns.mysampledomain.ch IN SOA ns1.mysampledomain.ch. hostmaster.mysampledomain.ch. (
                         2022031901 ; serial
                                  1 ; refresh (1 second)
                                  1 ; retry (1 second)
                              86400 ; expire (1 day)
                                  1 ; minimum (1 second)
                          )
                          NS ns1.mysampledomain.ch.
                          NS ns2.mysampledomain.ch.
                          A  172.29.1.140
$ORIGIN dyndns.mysampledomain.ch.
$TTL 60 ; 1 minute
test                      A  8.8.8.8

Nach einem Neustart des DNS-Servers mit

/bin/systemctl restart named-chroot.service

sollten die folgenden Abfragen zu dieser Ausgabe führen:

[root@almalinux85 etc]# dig www.google.ch +short @127.0.0.1
172.217.168.35
[root@almalinux85 etc]# dig www.google.ch +short @172.29.1.140
[root@almalinux85 etc]# dig ns1.mysampledomain.ch +short @127.0.0.1
172.29.1.140
[root@almalinux85 etc]# dig ns1.mysampledomain.ch +short @172.29.1.140
172.29.1.140
[root@almalinux85 etc]# dig test.dyndns.mysampledomain.ch +short @127.0.0.1
8.8.8.8
[root@almalinux85 etc]# dig test.dyndns.mysampledomain.ch +short @172.29.1.140
8.8.8.8

www.google.ch sollte sich also nur über localhost abfragen lassen, die selber verwaltete Domain mysampledomain.ch hingegen auch über die externe IP.

Slave-Server inkl. Zone Transfer, einrichten des DNS-Servers bei der Domain Registrierungsstelle, allfällige Firewall-Einstellungen für Abfragen von extern (Port 53) sind nicht Bestandteil dieser Dokumentation.

Web-Server vorbereiten

dnf install httpd php
systemctl enable httpd.service
/bin/systemctl start httpd.service

Neuere PHP-Versionen, Einrichtung von Virtual Hosts etc. sind nicht Bestandteil dieser Dokumentation. Standardmässig befindet sich die Webseite unter /var/www/html, auf dieses Verzeichnis wird nachfolgend verwiesen und muss ggf. angepasst werden.

Script, welches Update-Anfragen von Clients entgegennimmt und speichert (PHP)

Das nachfolgende Script kann von einem Client aufgerufen werden (z.B. regelmässig über einen Cronjob/Geplanten Task) und übergibt einen Hostnamen. Optional kann noch eine IP-Adresse übergeben werden, ist dies nicht der Fall wird die PHP-Variable $_SERVER['REMOTE_ADDR'] verwendet, welche der öffentlichen IP des Besuchers entspricht. Das Script erstellt daraufhin ein DNS-Updatescript, welches im nächsten Schritt von einem Cronjob auf dem Server verarbeitet wird.

Wichtig: Das hier aufgelistete PHP-Script erlaubt den Adressupdate für beliebige Hostnamen der DynDNS-Subdomain auf beliebige IPs ohne jede Art von Benutzerauthentifizierung, Hier sollte natürlich unbedingt eine Benutzerverwaltung mit erlaubtem Hostnamen pro Benutzer implementiert werden. Ebenfalls ist keine Fehlerausgabe implementiert, wenn sich die Textdatei nicht erstellen lässt. Dies alles lässt sich mit entsprechenden PHP-Kenntnissen und passenden Frameworks problemlos umsetzen. Das minimale Script zeigt das Zusammenspiel mit dem DNS-Server auf und lässt sich so auch problemlos auf andere Programmiersprachen erweitern, falls PHP nicht das gewünschte Mittel darstellt. Theoretisch lässt sich das sogar über ein eigenes Programm abdecken. Grundsätzlich muss nur Hostnamen und optional eine IP entgegengenommen und dann eine Textdatei erstellt werden.

Die PHP-Datei /var/www/html/index.php mit folgendem Inhalt

<?php
header("Content-Type: text/plain");
if ((isset($_REQUEST['hostname'])) and (preg_match("/^([a-zA-Z\d\-]+)$/", $_REQUEST['hostname']))) {
  $hostname = $_REQUEST['hostname'];
  $ip = $_SERVER['REMOTE_ADDR'];
  if ((isset($_REQUEST['ip'])) and (preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\$/", $_REQUEST['ip'])))
    $ip = $_REQUEST['ip'];
  $dyndomain = 'dyndns.mysampledomain.ch';
  if (!is_dir(__DIR__.'/data/'))
    mkdir(__DIR__.'/data/');
  $fh = fopen(__DIR__.'/data/'.$hostname.'.ip', 'w');
  fwrite($fh, "update delete ".$hostname.".".$dyndomain." A\n");
  fwrite($fh, "update add ".$hostname.".".$dyndomain." 60 A ".$ip."\n");
  fwrite($fh, "show\n");
  fwrite($fh, "send\n");
  fclose($fh);
  echo "OK, hostname '".$hostname."' updated with IP '".$ip."'";
} else {
  echo "ERROR: missing/invalid hostname";
}

lässt sich wie folgt aufrufen:

http://dyndns.mysampledomain.ch/?hostname=hallowelt
oder
http://dyndns.mysampledomain.ch/?hostname=hallowelt&ip=2.2.2.2

Beide Aufrufe updaten die Subdomain hallowelt.dyndns.mysampledomain.ch, beim ersten Aufruf mit der öffentlichen IP des Besuchers, beim zweiten Aufruf mit der fixen IP 2.2.2.2. Beim Aufruf erstellt das PHP-Script im Ordner /var/www/html/data eine Datei hallowelt.ip mit folgendem Inhalt:

update delete hallowelt.dyndns.mysampledomain.ch A
update add hallowelt.dyndns.mysampledomain.ch 60 A 2.2.2.2
show
send

Diese Datei wird durch den nachfolgenden Cronjob verarbeitet. Das Verzeichnis data sollte existieren und durch den Webserver beschreibbar sein.

Update Cronjob

Die mit dem PHP erstellten Update-Dateien können nun via nsupdate und einen Cronjob importiert werden. Dazu muss als erstes mal der Update-Key für den DNS-Server in eine Datei update.key kopiert werden. Key und Shellscript sollten sich in einem nicht öffentlich zugänglichen Verzeichnis befinden, ich verwende hier einmal /root/dyndns/update.key

key "ddns-key.dyndns.mysampledomain.ch" {
  algorithm hmac-sha256;
  secret "BxUCvbBoN/BzQjGEbJpWZ9BaZArgBeB/XOkuzkTIPMA=";
};

Die Kommandozeile für einen Update sieht dann so aus:

/bin/cat /var/www/html/data/hallowelt.ip |/usr/bin/nsupdate -k /root/dyndns/update.key

Dies sollte einmal so getestet werden, ob das auch mit Status NOERROR endet. Ein REFUSED(BADKEY) deutet auf einen abweichenden Key hin, oder ein Problem mit dem DNS-Server: nsupdate verwendet den in /etc/resolv.conf eingetragenen DNS-Server, dort sollte allenfalls 127.0.0.1 erfasst werden. Bei SERVFAIL ist die Logdatei /var/named/chroot/var/named/data/named.run allenfalls ein guter Punkt zur Fehlersuche. Ich vergesse immer die Schreibrechte auf das pri Verzeichnis, so das die Journal-Datei nicht erstellt werden kann...

Funktioniert manuell alles, kann ein kleines Shell-Script erstellt werden, welche alle Dateien in dem Verzeichnis verarbeitet und dann löscht:

#!/bin/bash
for updatehost in /var/www/html/data/*.ip
do
  if [ -e $updatehost ]; then
    /bin/cat $updatehost |/usr/bin/nsupdate -k /root/dyndns/update.key
    /bin/rm $updatehost
  fi
done

Und dann kann mit crontab -e noch ein minütlicher Eintrag in die Crontab erstellt werden

* * * * * /root/dyndns/updatedns.sh > /dev/null 2>&1

Damit sollte alles funktionieren.

Bei dieser Anleitung handelt es sich ja um eine etwas komplexere Sache. Konnte diese Anleitung weiterhelfen? Was war allenfalls noch unklar? Wo bestanden oder bestehen noch Probleme bei der Umsetzung? Ich würde mich über Feedback via Kontaktseite freuen!