Speedport Smart 3/4 und DynV6: Wenn DynDNS fast funktioniert

Wer einen Router der Telekom-Speedport-Smart-Serie betreibt und DynDNS nutzen möchte, stößt früher oder später auf eine merkwürdige Einschränkung: Die Router unterstützen zwar benutzerdefinierte DynDNS-Anbieter, lassen aber nur sehr eingeschränkt konfigurieren, wie das eigentliche Update durchgeführt wird.

Für viele klassische DynDNS-Anbieter reicht das aus. Bei modernen Diensten wie DynV6 wird es jedoch interessant.

Warum überhaupt DynV6?

Ich nutze für verschiedene Zwecke den kostenlosen DynDNS-Dienst DynV6. Der Dienst unterstützt sowohl IPv4 als auch IPv6 und bietet mehrere Möglichkeiten zur Aktualisierung von DNS-Einträgen. Neben einer klassischen DynDNS-Schnittstelle existiert auch eine moderne Update-API, die gleichzeitig IPv4- und IPv6-Adressen aktualisieren kann.

Gerade bei heutigen Dual-Stack-Anschlüssen ist das sinnvoll, denn viele Geräte besitzen sowohl eine öffentliche IPv4- als auch eine IPv6-Adresse.

Die offizielle DynV6-Update-API erwartet beispielsweise Parameter wie:

https://dynv6.com/api/update?hostname=example.dynv6.net&token=TOKEN&ipv4=1.2.3.4&ipv6=2001:db8::1

und kann damit beide Adressfamilien in einem einzigen Aufruf aktualisieren.

Das Problem mit dem Speedport

Die Speedport-Smart-Serie erlaubt zwar die Konfiguration eines benutzerdefinierten DynDNS-Anbieters, jedoch nicht die freie Definition der eigentlichen Update-URL.

Stattdessen verwendet die Firmware intern ein fest eingebautes Schema, das sich am klassischen DynDNS-Protokoll orientiert.

Der Router sendet dabei Anfragen in der Form:

GET /nic/update

mit Parametern wie:

hostname=meinhost.dynv6.net
myip=2003:....,93.205.62.84

und HTTP-Basic-Authentication.

Interessanterweise übermittelt der Speedport dabei bereits sowohl die IPv6- als auch die IPv4-Adresse im Parameter myip.

Ein typischer Request sieht beispielsweise so aus:

hostname=meine-dyndns-domain.dynv6.net
myip=2003:ca:27ff:39b:aeb6:87ff:fe53:a20d,93.205.62.84

Der Router liefert also eigentlich alle notwendigen Informationen – verwendet aber nicht die moderne DynV6-API.

Die Eigenheit von DynV6

DynV6 unterstützt zusätzlich eine Kompatibilitätsschnittstelle zum historischen DynDNS-Protokoll. Diese wurde ursprünglich für ältere Router und Clients implementiert.

Laut Dokumentation kann diese DynDNS-kompatible Schnittstelle jedoch nur IPv4-Adressen aktualisieren. Für IPv6 ist die Update-API vorgesehen.

Genau hier entsteht die unschöne Kombination:

  • Der Speedport kennt nur sein fest verdrahtetes DynDNS-Schema.
  • DynV6 bietet dafür zwar Kompatibilität.
  • Die moderne IPv6-Unterstützung steckt aber in einer anderen API.

Die Lösung: Ein kleiner Proxy

Da ich ohnehin Webspace mit PHP zur Verfügung habe, habe ich einen kleinen Proxy geschrieben.

Der Speedport spricht dabei nicht direkt mit DynV6, sondern mit meinem Skript:

Speedport
    │
    ▼
PHP-Proxy
    │
    ▼
DynV6 API

Der Proxy übernimmt dabei mehrere Aufgaben:

  1. Entgegennahme des Speedport-Requests
  2. Zerlegen des Parameters myip
  3. Erkennung von IPv4 und IPv6
  4. Umsetzung auf die DynV6-Update-API
  5. Protokollierung aller Updates

Aus

hostname=meine-dyndns-domain.dynv6.net
myip=2003:ca:27ff:39b:aeb6:87ff:fe53:a20d,93.205.62.84

wird intern:

https://dynv6.com/api/update
    ?hostname=meine-dyndns-domain.dynv6.net
    &token=...
    &ipv4=93.205.62.84
    &ipv6=2003:ca:27ff:39b:aeb6:87ff:fe53:a20d

Genau so, wie es die DynV6-API erwartet.

Warum nicht einfach ddclient?

Natürlich ließe sich das Problem auch mit einem DynDNS-Client wie ddclient auf einem Raspberry Pi lösen. Ein solcher Client muss die aktuelle öffentliche IPv4- und IPv6-Adresse jedoch zunächst selbst ermitteln – beispielsweise über externe Webdienste oder spezielle Router-Schnittstellen. Der Speedport kennt seine WAN-Adressen dagegen bereits und übermittelt sie sogar korrekt im DynDNS-Request. Das hier vorgestellte Skript nutzt daher die vorhandene DynDNS-Funktion des Routers und übersetzt lediglich den Request in die von DynV6 erwartete API-Struktur. Zusätzliche Dienste zur Adressermittlung sind dadurch nicht erforderlich.

Nebenbei entstand ein nützliches Monitoring

Ursprünglich diente das Skript nur dazu, die genaue Update-Anfrage des Speedports zu analysieren.

Inzwischen protokolliert es strukturierte JSON-Logs wie:

{
  "time":"2026-06-10 12:35:23",
  "remote_ip":"93.205.62.84",
  "hostname":"meine-dyndns-domain.dynv6.net",
  "ipv4":"93.205.62.84",
  "ipv6":"2003:ca:27ff:39b:aeb6:87ff:fe53:a20d",
  "http":200,
  "result":"addresses updated",
  "router":"Speedport Smart 4 Typ A",
  "firmware":"010139.5.0.001.0"
}

Dadurch lässt sich später sehr einfach nachvollziehen:

  • wann der Router Updates geschickt hat,
  • welche IP-Adressen übermittelt wurden,
  • welche Firmware beteiligt war,
  • ob DynV6 die Aktualisierung akzeptiert hat.

Ein unerwarteter Nebeneffekt: Pi-hole

Während der Entwicklung fiel noch ein weiteres Problem auf.

Meine DynV6-Domain wurde lokal von Pi-hole auf 0.0.0.0 beziehungsweise :: umgebogen.

Die Ursache war eine Malware-Blockliste, die pauschal die komplette Domain dynv6.net blockierte.

Ein klassischer Fall von Overblocking:

||dynv6.net^

Dadurch lieferten die lokalen DNS-Server völlig andere Antworten als öffentliche Resolver.

Erst nach einer Whitelist-Ausnahme funktionierte die lokale Namensauflösung wieder korrekt.

Fazit

Der eigentliche Fehler liegt weder bei DynV6 noch bei der Telekom. DynV6 stellt eine moderne API bereit, die IPv4 und IPv6 sauber unterstützt. Der Speedport wiederum liefert sogar beide Adressen korrekt mit. Beide Systeme sprechen jedoch unterschiedliche Dialekte derselben Sprache. Ein kleiner Übersetzer in Form eines PHP-Proxys genügt, um diese Lücke zu schließen.

Das Ergebnis ist eine zuverlässige DynDNS-Lösung für Speedport Smart 3 und Smart 4, die sowohl IPv4 als auch IPv6 aktualisiert und nebenbei eine brauchbare Protokollierung aller Updates liefert.

Manchmal besteht die Lösung eines Netzwerkproblems eben nicht darin, ein System auszutauschen, sondern zwei Systeme dazu zu bringen, dieselbe Sprache zu sprechen.

Quellcodes

Und hier ist der Quellcode meines Skriptes update.php:

<?php

error_reporting(E_ALL);
ini_set('display_errors', 0);

$now = date('Y-m-d H:i:s');
$today = date('Y-m-d');

const UPDATE_URI = 'https://dynv6.com/api/update';

$allowed_hosts = [
	'shikon-no-tama.dynv6.net',
	'rats-apo-mak.dynv6.net'
];

$logdir = __DIR__ . '/log';

$logfile = $logdir . '/update-' . $today . '.log';
$errorfile = $logdir . '/error-' . $today . '.log';

function log_event(string $file, array $data): void
{
	$fp = fopen($file, 'a');

	if ($fp === false) {
		return;
	}

	$json = json_encode(
		$data,
		JSON_UNESCAPED_SLASHES |
		JSON_UNESCAPED_UNICODE
	);

	if ($json === false) {
		return;
	}

	flock($fp, LOCK_EX);
	fwrite($fp, $json . PHP_EOL);

	flock($fp, LOCK_UN);
	fclose($fp);
}

function fail(int $httpcode, string $message, array $context = []): never
{
	global $errorfile;

	$logdata = array_merge(
		[
			'time' => date('Y-m-d H:i:s'),
			'http' => $httpcode,
			'error' => $message
		],
		$context
	);

	log_event($errorfile, $logdata);

	http_response_code($httpcode);
	echo $message;
	exit;
}

$remote_ip = $_SERVER['REMOTE_ADDR'] ?? '-';
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '-';

$hostname = trim($_GET['hostname'] ?? '');
$myip = trim($_GET['myip'] ?? '');

if ($hostname === '') {
	fail(400, 'missing hostname', ['remote_ip' => $remote_ip, 'ua' => $user_agent]);
}

if ($myip === '') {
	fail(400, 'missing myip', ['remote_ip' => $remote_ip, 'ua' => $user_agent]);
}

if (!isset($_SERVER['PHP_AUTH_PW'])) {
	fail(401, 'missing token', ['remote_ip' => $remote_ip, 'ua' => $user_agent]);
}

if (!in_array($hostname, $allowed_hosts, true)) {
	fail(403, 'hostname not allowed', ['remote_ip' => $remote_ip, 'ua' => $user_agent]);
}

/*
 * Dynv6 verwendet den Token als Passwort.
 */
$token = trim($_SERVER['PHP_AUTH_PW']);

$ipv4 = null;
$ipv6 = null;

foreach (explode(',', $myip) as $ip) {

	$ip = trim($ip);

	if (
		filter_var(
			$ip,
			 FILTER_VALIDATE_IP,
			 FILTER_FLAG_IPV4
		)
	) {
		$ipv4 = $ip;
		continue;
	}

	if (
		filter_var(
			$ip,
			 FILTER_VALIDATE_IP,
			 FILTER_FLAG_IPV6
		)
	) {
		$ipv6 = $ip;
	}
}

if ($ipv4 === null && $ipv6 === null) {
	fail(400, 'no valid IP found', ['remote_ip' => $remote_ip, 'ua' => $user_agent]);
}

/*
 * Dynv6 API URL aufbauen
 */

$params = [
	'hostname' => $hostname,
	'token'    => $token
];

if ($ipv4 !== null) {
	$params['ipv4'] = $ipv4;
}

if ($ipv6 !== null) {
	$params['ipv6'] = $ipv6;
}

$url = UPDATE_URI . '?' . http_build_query($params);

$ch = curl_init();

curl_setopt_array(
	$ch,
	[
		CURLOPT_URL => $url,
		CURLOPT_RETURNTRANSFER => true,
		CURLOPT_CONNECTTIMEOUT => 10,
		CURLOPT_TIMEOUT => 20,
		CURLOPT_USERAGENT => $user_agent
	]
);

$result = curl_exec($ch);

$curl_error = curl_error($ch);

$httpcode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

curl_close($ch);

$router = null;
$firmware = null;

if (preg_match('/Deutsche Telekom - (.+?) - ([0-9.]+)$/', $user_agent, $matches)) {
	$router = trim($matches[1]);
	$firmware = trim($matches[2]);
}

$result = trim((string)$result);

if ($result === 'addresses unchanged') {
	$reply = 'nochg';
} elseif ($result === 'addresses updated') {
	$reply = 'good';
} else {
	$reply = $result;
}

$logdata = [
	'time'      => $now,
	'remote_ip' => $remote_ip,
	'hostname'  => $hostname,
	'ipv4'      => $ipv4,
	'ipv6'      => $ipv6,
	'http'      => $httpcode,
	'result'    => $result,
	'reply'		=> $reply,
	'ua'        => $user_agent,
	'router'	=> $router,
	'firmware'	=> $firmware,
	'query' 	=> $_SERVER['QUERY_STRING'] ?? null
];

if ($result === false) {

	$logdata['curl_error'] = $curl_error;

	fail(502, 'curl error', $logdata);

} elseif ($httpcode !== 200) {

	$logdata['dynv6_error'] = $result;

	fail(502, 'dynv6 returned HTTP ' . $httpcode, $logdata);
}

log_event($logfile, $logdata);

echo $reply;
exit;

Das Skript sollte idealerweise im Document Root einer eigenen Subdomain z.B. dyn.meine-domain.de liegen. Darin sollte eine entsprechende .htaccess vorhanden sein:

RewriteEngine On

RewriteBase /
RewriteRule ^update\.php$ - [L]
RewriteRule ^nic(.*) /update.php [QSA,L]

Weiter Voraussetzung ist außerdem ein Unterverzeichnis /log in welches die Logs geschrieben werden und was per .htaccess vor Webzugriffen geschützt sein sollte. Alternativ kann man das Log-Verzeichnis natürlich auch außerhalb des Document Roots anlegen und den Pfad im Skript anpassen:

deny from all

Nachtrag

Nach ersten Praxistests zeigte sich außerdem ein Unterschied zwischen Speedport Smart 3 und Smart 4. Während der Smart 4 einen Telekom-spezifischen User-Agent inklusive Modell- und Firmwareinformationen sendet, meldet sich der Smart 3 lediglich als „inadyn/2.1“. Dies deutet darauf hin, dass die DynDNS-Funktion des Smart 3 direkt auf dem Open-Source-Projekt inadyn basiert. Zudem scheint der Smart 3 in meinem Test nur die IPv4-Adresse zu übermitteln, während der Smart 4 sowohl IPv4 als auch IPv6 in einem Request sendet.

Speedport Smart 3

inadyn/2.1 https://github.com/troglobit/inadyn/issues

Speedport Smart 4

Deutsche Telekom - Speedport Smart 4 Typ A - 010139.5.0.001.0

Nachtrag 2: Manche mögens klassisch

Ein besonders interessanter Stolperstein zeigte sich nicht auf der Eingangsseite des DynDNS-Updates, sondern bei der Interpretation der Antwort des Zielsystems. DynV6 arbeitet modern und semantisch: Statt klassischer DynDNS-Rückgabecodes wie „good“ oder „nochg“ liefert der Dienst verständliche Statusmeldungen wie „addresses updated“ oder „addresses unchanged“.

Der Speedport Smart 3 hingegen basiert in meinem Fall offensichtlich auf einem inadyn-Client, der noch strikt auf das klassische DynDNS2-Protokoll ausgelegt ist. Dieses erwartet sehr einfache, fest definierte Rückgabewerte. Alles, was davon abweicht, wird nicht als Erfolg interpretiert – selbst dann, wenn der HTTP-Status eindeutig „200 OK“ signalisiert.

Das führt zu einem subtilen, aber praxisrelevanten Problem: Das Update ist technisch erfolgreich abgeschlossen, wird vom Router aber dennoch als „nicht registriert“ dargestellt, weil er die Antwort semantisch nicht einordnen kann. Erst durch die gezielte Übersetzung der DynV6-Antwort in klassische DynDNS-Codes („addresses unchanged“ → „nochg“, „addresses updated“ → „good“) lässt sich diese Diskrepanz auflösen.

Das System verhält sich damit letztlich nicht inkorrekt, sondern spricht lediglich zwei unterschiedliche „Dialekte“ desselben Protokolls. Der Speedport erwartet die alte, knappe Rückgabesprache des DynDNS2-Standards, während DynV6 bewusst eine modernisierte, besser lesbare Antwortstruktur verwendet. Das hier vorgestellte Skript übernimmt daher nicht nur die Weiterleitung der Updates, sondern auch die Übersetzung dieser Protokolldialekte – und stellt damit die erwartete Kompatibilität für die Router-Firmware wieder her.

Schreibe einen Kommentar