Einrichtung eines unabhängigen DynDNS Dienstes mit PowerDNS und MySQL/MariaDB
Da ich es eher nervig fand, aller 30 Tage die Aktivität meiner URL bei einem DynDNS Anbieter zu bestätigen, entschied ich mich dazu, meinen eigenen DynDNS Dienst einzurichten. Nachfolgend werde ich erklären, wie ich dies umgesetzt habe.
Der Artikel wird sich dabei in die folgenden einzelnen Schritte aufteilen:
- Anlegen neuer Records beim Provider
- Einrichtung von PowerDNS
- Einrichtung der Datenbank
- Anlegen eigener Records
- Einrichtung der automatischen Aktualisierung von Records
- Cronjob
Die Voraussetzungen, um einen solchen DynDNS Dienst zu betreiben, erfüllte ich zum Start bereits:
- Server mit statischer, öffentlicher IP Adresse
- Eigene Domain
Auf Empfehlung meines Bruders entschied ich mich dazu, die Software PowerDNS einuzsetzen. Da ich ohnehin schon mehrere MariaDB Datenbanken auf meinem Server betreibe, habe ich diese als Backend zum speichern und abrufen der DNS Records eingesetzt. Hier kann allerdings auch etwas leichteres wie z.B SQLite genutzt werden.
Die Installation habe ich auf einem System mit Debian 9 vorgenommen. Solltet ihr eine andere Distribution nutzen, können Kommandos und Pakete abweichen.
Während meiner Anleitung werde ich nicht darauf eingehen, wie man die Installation eines Webservers (z.B. Apache, Nginx) oder eines Datenbankmanagementsystems (z.B. MySQL) vornimmt. Solltet ihr eines der beiden noch nicht installiert haben, solltet ihr dies vor der Abarbeitung meiner Anleitung erledigen.
Anlegen von NS und A Record beim Domain-Provider
Damit der DNS Server, welcher anschließend eingerichtet wird, auch aus dem Internet erreichbar ist, müssen die folgenden Einträge beim Anbieter gesetzt werden, bei welchem ihr eure Domain registriert habt.
A Record – um dem Internet mitzuteilen unter welcher Adresse der Nameserver zu erreichen ist.
ns1.beispiel.de A 10.0.0.0
NS Record – um dem Internet mitzuteilen, an welchen Nameserver alle Anfragen weitergeleitet werden sollen.
dyndns.beispiel.de NS ns1.beispiel.de
Einrichtung von PowerDNS
Zu Beginn werden die benötigten Pakete installiert:
apt-get install pdns-server
apt-get install pdns-backend-mysql
Nun gibt es mehrere möglichkeiten die Konfiguration der Datenbankverbindung in PowerDNS vorzunehmen. Ich habe hier die Variante der offiziellen powerdns Dokumentation präferiert, bei welcher die Datenbankverbindung in der pdns.conf unter /etc/powerdns/ eingetragen wird.
Dazu öffnete ich mir die Config mit nano:
nano /etc/powerdns/pdns.conf
und navigierte zu folgender Zeile: # launch Which backends to launch and order to query them in
Hier fügte ich dann meine zukünftigen Zugangsdaten zur Datenbank hinzu. Außerdem habe ich definiert, welches Backend genutzt werden soll. (Da die Datenbank zu dem Zeitpunkt noch nicht existiert, können die Zugangsdaten frei gewählt werden.)
launch=gmysql gmysql-host=127.0.0.1 gmysql-user=pdns gmysql-dbname=powerdns gmysql-password=euerpasswort
Anschließend schloss und speicherte ich die Datei.
Damit PowerDNS später keine Fehler beim neustart des Daemon ausgibt, habe ich die durch den Download automatisch erstellten Konfigurationen für andere Backends gelöscht. Die Fehler treten auf, wenn mehrere Backends definiert sind.
rm -rf /etc/powerdns/pdns.d/*
Einrichtung der Datenbank
Für die Datenbank, in welche PowerDNS später hineinschreibt, wird ein genaues Schema vorgegeben. Dieses habe ich für meine Installation übernommen.
Ich meldete mich also im DBMS an:
mysql -u root -p
Erstellte eine neue Datenbank namens powerdns:
CREATE DATABASE powerdns;
Erstellte einen Benutzer mit den Zugangsdaten, welche ich vorher in der Konfiguration von PowerDNS hinterlegt habe:
CREATE USER 'pdns'@'localhost' IDENTIFIED BY 'euerpasswort';
Erteilte dem Benutzer komplette Rechte zum verändern der Datenbank:
GRANT ALL PRIVILEGES ON powerdns.* TO 'pdns'@'localhost';
und lud die GRANT Tabellen neu:
FLUSH PRIVILEGES;
Die Vorbereitungen hatte ich somit erledigt. Danach führte ich stumpf die folgenden Befehle zur Erstellung des Datenbank-Schema aus.
USE powerdns;
CREATE TABLE domains (
id INT AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
master VARCHAR(128) DEFAULT NULL,
last_check INT DEFAULT NULL,
type VARCHAR(6) NOT NULL,
notified_serial INT UNSIGNED DEFAULT NULL,
account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE UNIQUE INDEX name_index ON domains(name);
CREATE TABLE records (
id BIGINT AUTO_INCREMENT,
domain_id INT DEFAULT NULL,
name VARCHAR(255) DEFAULT NULL,
type VARCHAR(10) DEFAULT NULL,
content VARCHAR(64000) DEFAULT NULL,
ttl INT DEFAULT NULL,
prio INT DEFAULT NULL,
disabled TINYINT(1) DEFAULT 0,
ordername VARCHAR(255) BINARY DEFAULT NULL,
auth TINYINT(1) DEFAULT 1,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
CREATE INDEX ordername ON records (ordername);
CREATE TABLE supermasters (
ip VARCHAR(64) NOT NULL,
nameserver VARCHAR(255) NOT NULL,
account VARCHAR(40) CHARACTER SET 'utf8' NOT NULL,
PRIMARY KEY (ip, nameserver)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE TABLE comments (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(10) NOT NULL,
modified_at INT NOT NULL,
account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
comment TEXT CHARACTER SET 'utf8' NOT NULL,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX comments_name_type_idx ON comments (name, type);
CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);
CREATE TABLE domainmetadata (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
kind VARCHAR(32),
content TEXT,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX domainmetadata_idx ON domainmetadata (domain_id, kind);
CREATE TABLE cryptokeys (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
flags INT NOT NULL,
active BOOL,
content TEXT,
PRIMARY KEY(id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX domainidindex ON cryptokeys(domain_id);
CREATE TABLE tsigkeys (
id INT AUTO_INCREMENT,
name VARCHAR(255),
algorithm VARCHAR(50),
secret VARCHAR(255),
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);
Anlegen einer Zone und hinzufügen von Records
Nachdem das Datenbank-Schema angelegt wurde, konnte ich damit beginnen, die Tabellen mit Daten zu füllen.
In meinem Beispiel ist der Nameserver für die Domain dyndns.beispiel.de zuständig. Alle DNS Anfragen für dyndns.beispiel.de und alle darunter liegenden Adressen (netz.dyndns.beispiel.de) werden an den Server weitergeleitet. (wurde im ersten Schritt festgelegt)
Damit der Server auf diese Anfragen antworten kann, musste eine Zone für die Domain eingerichtet werden. Der Zone konnte ich anschließend Records hinzufügen. Mit folgendem Befehl legte ich die Zone an:
INSERT INTO domains (name, type) values ('dyndns.beispiel.de', 'NATIVE');
Damit die Zone richtig funktioniert musste anschließend noch ein „SOA“ Record gesetzt werden. Hierbei habe ich mich auch wieder an die Angaben der Doku von PowerDNS gehalten.
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'dyndns.beispiel.de','localhost admin.beispiel.de 1 10380 3600
604800 3600','SOA',86400,NULL);
Nun konnte mit dem einpflegen der eigenen Records begonnen werden. Für das Beispiel habe ich einen A record angelegt, welcher die Anfragen an netz.dyndns.beispiel.de nach 1.1.1.1 auflöst.
INSERT INTO records (domain_id, name, content, type, ttl, prio) VALUES (1,'netz.dyndns.beispiel.de','1.1.1.1','A',120,NULL);
Die IP ist in diesem Anwendungsfall des DNS erstmal irrelevant, da sich diese nach Fertigstellung des Projektes aller paar Minuten (je nachdem wie oft sich eure öffentliche IP aktualisiert) automatisch aktualisieren soll.
Einrichtung einer automatischen Aktualisierung der Records
Damit der eben angelegte Record immer mit der IP des Gerätes bzw. des Heimnetzwerks aktualisiert wird, habe ich ein Script angewendet, welches die Daten, welche minütlich von einem Gerät im Netzwerk, oder einer FritzBox gesendet werden, in die Datenbank schreibt und somit den Record aktualisiert. Wie genau die Einrichtung der automatisierten Aktualisierung erfolgt, werde ich nun näher erklären.
Wie bereits erwähnt, setze ich als Bestandteil meiner Anleitung voraus, dass bereits ein Webserver auf dem Gerät eingerichtet ist, welches als DNS Server dient. Um das folgende Script nutzen zu können, muss außerdem noch PHP auf dem Server installiert sein.
Das Script, welches ich genutzt habe, stammt von Simon Lauger. Auf seiner Website hat er dieses veröffentlicht. Für meine Zwecke habe ich das Script minimal angepasst.
Da das Script später per HTTP/HTTPS erreicht werden soll, musste dieses an einer beliebigen Stelle im Webroot erstellt werden. In den meisten Fällen sollte das unter /var/www/ bzw. /var/www/html sein.
nano /var/www/html/dnsupdate.php
Hier kann nun der folgende Code eingefügt werden.
<?php
/**
DynDNS-Service für PowerDNS
*
@author Simon "cmon2k" Lauger <simon@lauger.name
@date 06.09.2012
*/
$dsn = 'mysql:dbname=powerdns;host=127.0.0.1'; // Datenbank DSN
$user = 'pdns'; // Name der Datenbank
$pass = 'euerpasswort'; // Datenbank Passwort
// Auth-String der als GET-Parameter übermittelt werden muss
$auth = 'eueranderespasswort';
// Für alle im Array enthaltenen Records dürfen Updates gefahren werden
$allowed = array('netz.dyndns.beispiel.de');
$domain = (isset($_GET['domain'])) ? $_GET['domain'] : null;
$ip = $_SERVER['REMOTE_ADDR'];
if ((empty($domain) || is_null($domain)) || (empty($ip) || is_null($ip))) {
die('missing parameter');
exit;
}
if (!in_array($domain, $allowed)) {
die('forbidden domain name');
exit;
}
if (!isset($_GET['passwd']) || $_GET['passwd'] != $auth) {
die('authentification failed');
exit;
}
try {
$dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
$check = $dbh->prepare('SELECT id FROM records WHERE name = :name AND type = :type');
$check->bindParam(':name', $domain);
$check->bindValue(':type', 'A');
$check->execute();
$result = $check->fetch(PDO::FETCH_ASSOC);
if (empty($result)) {
die('record not found');
exit;
} else {
$update = $dbh->prepare('UPDATE records SET content = :content WHERE id = :id LIMIT 1');
$update->bindParam(':content', $ip);
$update->bindParam(':id', $result['id']);
if ($update->execute()) {
die('update successful (' . htmlentities($ip, ENT_QUOTES) . ')');
exit;
}
die('update returned false');
exit;
}
?>
Die Variablen $dsn, $user und $pass müsst ihr mit euren eigenen Daten, welche ihr im Schritt „Einrichten der Datenbank“ festgelegt habt, befüllen.
In der Variable $auth müsst ihr ein Kennwort definieren, welches später von dem Gerät übermittelt wird, welches den DNS Record updaten soll. (würde das nicht entsprechend implementiert sein, könnte ja jeder den Record updaten)
In der Variable $domain wird noch der Record angegeben, der aktualisiert werden soll. (Beispielsweise für den weiter oben angelegten Record – netz.dyndns.beispiel.de)
Information: Von hier an werden die Schritte an dem Gerät durchgeführt, welches für das Updaten des Records zuständig ist.
Nachdem das Skript entsprechend angepasst wurde, kann man testen, ob es wie gewünscht per HTTP/HTTPS erreichbar ist. Je nachdem wo sich das Skript im Webroot befindet, kann dieses einfach aufgerufen werden.
Beispielsweise https://beispiel.de/dnsupdate.php
Der Text „missing parameter“ sollte erscheinen.
Um nun zu testen, ob sich der Record auch über das Skript updaten lässt, müssen beim Aufruf des Skriptes die Parameter $auth und $domain übermittelt werden, welche im Skript definiert wurden.
Mit den von mir weiter oben gemachten Angaben, würde das also wie folgt aussehen:
https://beispiel.de/dnsupdate.php?domain=netz.dyndns.beispiel.de&passwd=eueranderespasswort
Hier sollte der Text „update successful“ erscheinen.
Nun wurde in den Record die IP eingetragen, von welcher die Anfrage gesendet wurde. Im Idealfall, wenn kein VPN oder ähnliches genutzt wird, sollte diese die IP des Heimnetzes sein.
Um das Update zu automatisieren kann nun zwischen zwei Varianten gewählt werden. Die aktuelle IP kann dem DNS Server entweder über ein beliebiges Gerät im Netzwerk mitgeteilt werden, oder aber durch einen Router, welcher sowieso immer eingeschaltet ist. Nachfolgend werde ich beide Varianten erläutern.
Update durch beliebiges Gerät
Um das Update durch ein Gerät im Netz durchzuführen, kann ein Cronjob eingerichtet werden. Dieser ruft die URL mit den entsprechenden Variablen aller paar Minuten auf.
crontab -e
Mit folgendem Job wird realisiert, dass die aktuelle IP jede Minute an den DNS Server gesendet wird.
* * * * * curl 'https://beispiel.de/dnsupdate.php?domain=netz.dyndns.beispiel.de&passwd=eueranderespasswort >/dev/null 2>&1
Sollte sich die Adresse des Heimnetzes nun ändern, dauert es höchstens eine Minute, bis der DNS Server die aktuelle IP hat.
Update durch Router
Einige Router haben bereits vorgefertigte Masken, welche nur mit den entsprechenden Informationen des DNS Dienstes ausgefüllt werden.
Die Konfiguration für eine Fritzbox würde wie folgt aussehen.
Wenn dem Anschluss nun eine neue IP vom Provider zugewiesen wird, teilt die Fritzbox dem DNS Dienst die aktuelle IP mit.
Abschluss
Ich hoffe, dass ich mit meinem Artikel auch andere dabei unterstützen konnte, ihren eigenen DynDNS Dienst einzurichten.
Solltet ihr etwas nicht ganz verstanden haben, bin ich natürlich gerne bereit eure Fragen in den Kommentaren zu beantworten.
Vielen Dank für die tolle Anleitung.
Genau dieses Problem mit den 30 Tagen hat mich auch vor ein paar Tagen ziemlich genervt, so dass ich auf duckdns.org umgestiegen bin.
Ich wollte nur als Hinweis geben, dass man bei einer Fritzbox ein individuelles Update Kommando dafür hinterlegen kann. Fand ich in der Install Doku von Duckdns.
Bsp: https://www.duckdns.org/update?domains=exampledomain&token=a7c4d0ad-114e-40ef-ba1d-d217904a50f2&ip=&ipv6=
Könnte man bei deinem Skript auch so ähnlich realisieren, so dass die Fritzbox es nur aktualisiert, wenn sich die IP auch geändert hat. Man kann auch einfach duckdns.org verwenden, wenn man diese Einschränkung mit den 30 Tagen oder Bezahlen nicht haben möchte.
Hallo Nedy,
danke für deinen netten Kommentar. Deinen Ansatz finde ich gut. Das könnte ich so auch mal in Erwägung ziehen.
Für Leute, die Router ohne DynDNS Funktion nutzen, könnte meine Variante dennoch hilfreich sein. 🙂
Grüße
Moin,
Klitzekleine Anmerkung: wenn Du denn JOB von der Fritzbox/DSL-Router erledigen liesesst, würde unnötiger Traffic vermieden, da die das nur bei Starts und Reconnects des DSL-Streams schicken würde.
Gibt extra einen Konfigpunkt bei euner Fritzbox dafür.
Der da wäre?
Jap da hast du recht. Könnte ich so machen.
Strato und All-Inkl. bieten DynDNS für die dort hinterlegten Domains an.
Habe noch ein Problem entdeckt…
Wenn ich über axfr meine Daten mit einem slave server austauschen will ist es wichtig das die Seriennummer im soa eintrag bei jeder Änderung angepasst wird.
Das Skript macht das leider nicht. Ich kenne mich leider zu wenig mit PHP und MySQL aus um das selber an zu passen. Vlt. hat ja jemand eine Idee
Hallo,
konntest Du das inzw. lösen?
VG
Vielen Dank für die super Anleitung. Hat auf Anhieb gleich funktioniert.
Eine Frage: Die Variable $auth, die das Kennwort zwischen Gerät und PowerDNS Server darstellt, ist dann bezogen auf den jeweiligen Record der in der updater.php und dem eingetragenen Record in der Datenbank steht, richtig?
Wenn ich als weitere Records anlege, kann auch ein Bekannter den PowerDNS Server nutzen?
Wenn ich für mich ich.domain.de als Record in der Datenbank und in der update.php eingetragen und für einen Bekannten den Record bekannter.domain.de in der Datenbank stehen habe, bräuchte ich nur die $auth Variable/Kennwort abändern und das Update-Script unter einen anderen Namen (bekannterupdater.php) abspeichern?
Freut mich, wenn dir meine Anleitung helfen konnte. 🙂
Die Variable $auth dient lediglich dazu, sicherzustellen, dass nicht jeder beliebige den Record in der Datenbank ändern kann.
Theoretisch sollte das was du geschrieben hast genau so funktionieren.
Nochmal zusammen gefasst was zu tun wäre:
-Neuen Record in Datenbank anlegen
-Neues Script anlegen
-$auth String anpassen
-$domain String anpassen
-Script speichern
Beste Grüße,
Bjarne
Hallo erstmal,
vielen Dank für die Anleitung, es ist die (von den Vielen die es gibt), die mich dem Ziel am weitesten gebraht hat, aber leider noch nicht ganz.
Mein Problem ist der in das Script, ich kann leider nicht klar erkennen wio ich die Domain eintragen muss, weil es „$domain“ an mehreren Stellen gibt: in Zeile 19, 2* in Zeile 22, in Zeile 27, und inZeile 44 – wenn ich keines übersehen habe.
das Ergebnis des Skriptes ist dann „record not found“
Ich habe dann spasseshalber in Zeile 12 folgenden Eintrag ergänzt:
$domain = ‚meine.Domainde‘; // Name der Domain
Ergebnis ist dasselbe
Ein weiterer Versuch mit allen „$domain“ durch ‚meine.Domainde‘ eretzt führt zu ei nem leeren Bildschirm.
Von aussen ist die Internetseite erreichbar, aud dem eigenen (W)LAN leider nicht. Wenn sich die IP malämdern sollte ist von außen vermutlich auch Ende im Gelände.
Über eine kleine Hilfestellung würde ich mich sehr freuen
Gruß
Christian