Sie können die komplette DNS-Zone auch über CURL oder ähnliches Programm abändern. Unser eigener Opensource-Client wird aktuell überholt. Hier eine kurze Anleitung, wie die Nutzung über CURL funktioniert. Für die Nutzung müssen Sie zuvor die API-ID der jeweiligen Domain aus dem Kundenportal generieren.
Schritt 1: Zone mit API-ID abfragen
Zone mit API-ID abfragen (Achtung, abschliessender /)
curl https://domainexpress.de:443/api/dns_zone/my_zone/YRAe7AYw06JKM1Pc8plp14_SoLgEyYItM3u5TxabJUyBuFwE6uCAxtGYfsHBCQbI/
a::1/source: live
a::1/target: 31.16.98.28
mx::1/prio: 10
mx::1/source: @
mx::1/target: x103.domainexpress.de
ns::1: ns1.hofmeirmedia.net
ns::2: ns2.hofmeirmedia.net
txt::1/source: @
txt::1/target: v=spf1 mx a ~all
txt::2/source: @
txt::2/target: v=DMARC1;p=quarantine;pct=100;rua=mailto:support@domainexpress.de
Schritt 2: Zone in Datei schreiben oder umlenken
curl https://domainexpress.de:443/api/dns_zone/my_zone/YRAe7AYw06JKM1Pc8plp14_SoLgEyYItM3u5TxabJUyBuFwE6uCAxtGYfsHBCQbI/ > zone.txt
Schritt 3: Zone editieren, anhängen / tauschen
Zum Beispiel anfügen:
txt::3/source: @
txt::3/target: diesisteintest
Schritt 4: Zone zurückspielen mit CURL
curl https://domainexpress.de:443/api/dns_zone/my_YItM3u5TxabJUyBuFwE6uCAxtGYfsHBCQbI/ -X POST --data-binary @zone.txt
Extra: Benachrichtigung via Email
Wenn Sie eine Benachrichtigung via Email wünschen, nachdem der DNS-Eintrag beim DNS-Server geändert wurde, fügen Sie bitte folgende Zeile mit dem Label email::1/notify: und Ihrer Mailadresse als weiteren Zonen-Eintrag in die zone.txt-Datei hinzu.
email::1/notify: ihreemail@ihreemail
Beispielhaftes Skript um eine Subdomain auf eine dynamische, externe IP zeigen zu lassen:
#!/bin/bash
# Stoppe bei Fehlern, z.B. kein Internetaccess
set -euo pipefail
# Optionale Verifikation per DNS-TXT-Record last_change (kann per ENV/CLI überschrieben werden)
VERIFY_LOOP="${VERIFY_LOOP:-0}"
while [[ $# -gt 0 ]]; do
case "$1" in
--verify)
VERIFY_LOOP=1
shift
;;
-h|--help)
echo "Usage: $0 [--verify]"
echo " --verify Warte nach dem Update darauf, dass der TXT-Record last_change im DNS erscheint."
exit 0
;;
*)
echo "Unbekannte Option: $1"
exit 1
;;
esac
done
# Prüfen, ob benötigte Programme vorhanden sind
for cmd in curl dig mktemp date; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Fehler: benötigtes Programm '$cmd' wurde nicht gefunden."
exit 1
fi
done
# Konfiguration (über ENV-Variablen überschreibbar)
ZONE="${ZONE:-liveconfig-bei-domainexpress.de}" # Zonename / Domain (für DNS-Abfragen)
TARGETSUB="${TARGETSUB:-www3}" # zu aktualisierender Host (source)
API_ID="${API_ID:-API_ID}" # API-ID aus dem Kundenportal
EMAIL_NOTIFY="${EMAIL_NOTIFY:-}" # optional: Mailadresse für Benachrichtigung, z.B. "user@example.com"
NS="${NS:-ns1.hofmeirmedia.net}" # autoritativer Nameserver für Prüfungen
# Die eigentliche Zone wird ausschließlich über die API-ID identifiziert.
ZONE_URL="https://domainexpress.de:443/api/dns_zone/my_zone/$API_ID/"
# "Aktuelle" IP aus DNS (direkt beim Authoritativen) und externe IP holen
IP_NOW=$(dig @"$NS" +short A "$TARGETSUB.$ZONE" | head -n1 || true)
IP_NOW=${IP_NOW%$'\n'}
IP=$(curl -fsS http://v4.liveconfig-bei-domainexpress.eu/)
# Für IPv6 stattdessen:
# IP=$(curl -s http://v6.liveconfig-bei-domainexpress.eu/)
if [ -z "$IP" ]; then
echo "Fehler: Konnte externe IPv4-Adresse nicht ermitteln." >&2
exit 1
fi
if [ "$IP_NOW" = "$IP" ] && [ -n "$IP" ]; then
echo "DNS bereits aktuell ($IP)"
exit 0
fi
echo "Starte Update auf $IP für $TARGETSUB.$ZONE"
TMPFILE="$(mktemp)"
trap 'rm -f "$TMPFILE"' EXIT
# Schritt 1+2: Zone mit API-ID abfragen und in Datei schreiben
curl -fsS "$ZONE_URL" -o "$TMPFILE"
# Passenden A-Record für TARGETSUB suchen
RECORD_ID=$(grep -E "^a::[0-9]+/source: $TARGETSUB\$" "$TMPFILE" | sed -E 's/^a::([0-9]+)\/source.*/\1/' | head -n1 || true)
if [ -n "$RECORD_ID" ]; then
echo "Gefundener bestehender A-Record mit ID $RECORD_ID – aktualisiere target"
# target-Zeile für diesen Record ersetzen
sed -E "s/^(a::$RECORD_ID\/target: ).*/\1$IP/" "$TMPFILE" > "${TMPFILE}.new"
mv "${TMPFILE}.new" "$TMPFILE"
else
echo "Kein bestehender A-Record für $TARGETSUB gefunden – füge neuen Eintrag hinzu"
LAST_ID=$(grep -E "^a::[0-9]+/source:" "$TMPFILE" | sed -E 's/^a::([0-9]+)\/.*/\1/' | sort -n | tail -n1)
if [ -z "$LAST_ID" ]; then
RECORD_ID=1
else
RECORD_ID=$((LAST_ID + 1))
fi
{
echo "a::${RECORD_ID}/source: $TARGETSUB"
echo "a::${RECORD_ID}/target: $IP"
} >> "$TMPFILE"
fi
# TXT-Record last_change setzen/aktualisieren
LAST_CHANGE_VALUE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
TXT_ID=$(grep -E "^txt::[0-9]+/source: last_change\$" "$TMPFILE" | sed -E 's/^txt::([0-9]+)\/source.*/\1/' | head -n1 || true)
if [ -n "$TXT_ID" ]; then
echo "Aktualisiere TXT-Record last_change (ID $TXT_ID) auf $LAST_CHANGE_VALUE"
sed -E "s/^(txt::$TXT_ID\/target: ).*/\1$LAST_CHANGE_VALUE/" "$TMPFILE" > "${TMPFILE}.new"
mv "${TMPFILE}.new" "$TMPFILE"
else
echo "Füge neuen TXT-Record last_change mit Wert $LAST_CHANGE_VALUE hinzu"
LAST_TXT_ID=$(grep -E "^txt::[0-9]+/source:" "$TMPFILE" | sed -E 's/^txt::([0-9]+)\/.*/\1/' | sort -n | tail -n1)
if [ -z "$LAST_TXT_ID" ]; then
TXT_ID=1
else
TXT_ID=$((LAST_TXT_ID + 1))
fi
{
echo "txt::${TXT_ID}/source: last_change"
echo "txt::${TXT_ID}/target: $LAST_CHANGE_VALUE"
} >> "$TMPFILE"
fi
# Optional: Email-Benachrichtigung hinzufügen
if [ -n "$EMAIL_NOTIFY" ]; then
if ! grep -q "^email::1/notify:" "$TMPFILE"; then
echo "email::1/notify: $EMAIL_NOTIFY" >> "$TMPFILE"
fi
fi
# Schritt 4: Zone zurückspielen mit CURL
curl -sS "$ZONE_URL" -X POST --data-binary @"$TMPFILE"
echo
echo "Update abgeschlossen."
# Optional: Warten, bis last_change-TXT im DNS sichtbar ist
if [ "$VERIFY_LOOP" -eq 1 ]; then
echo "Prüfe, ob TXT last_change im DNS erscheint ..."
# MAX_WAIT 5 Minuten bei direkter Abfrage am Authoritativen
MAX_WAIT=300 # maximale Wartezeit in Sekunden
INTERVAL=15 # Sekunden zwischen den Prüfungen
waited=0
while [ "$waited" -lt "$MAX_WAIT" ]; do
DNS_VALUE=$(dig @"$NS" +short TXT "last_change.$ZONE" | head -n1 | sed -e 's/^"//' -e 's/"$//')
if [ "$DNS_VALUE" = "$LAST_CHANGE_VALUE" ]; then
echo "Änderung im DNS bestätigt: last_change=$DNS_VALUE"
break
fi
sleep "$INTERVAL"
waited=$((waited + INTERVAL))
done
if [ "$waited" -ge "$MAX_WAIT" ]; then
echo "Warnung: last_change-TXT wurde nach ${MAX_WAIT}s noch nicht im DNS gesehen."
fi
fi
Sowie Perl-Variante:
#!/usr/bin/env perl
# -*- coding: utf-8 -*-
# dexdns.pl – DynDNS-Update für Domainexpress API (Zonen-basiert)
# Konfiguration über ENV: ZONE, TARGETSUB, API_ID, EMAIL_NOTIFY, NS, VERIFY_LOOP
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
use File::Temp qw(tempfile);
use HTTP::Tiny;
use POSIX qw(strftime);
use Socket qw(inet_ntoa);
my $VERIFY_LOOP = $ENV{VERIFY_LOOP} // 0;
my $DEBUG = $ENV{DEBUG} // 0;
my $opt_ip = '';
my $opt_ipv6 = 0;
GetOptions(
'verify!' => \$VERIFY_LOOP,
'debug!' => \$DEBUG,
'ip=s' => \$opt_ip,
'ipv6|6' => \$opt_ipv6,
'help|h' => sub {
print "Usage: $0 [--verify] [--no-verify] [--ip ADRESSE] [--ipv6]\n";
print " --verify Nach dem Update warten, bis last_change im DNS erscheint.\n";
print " --no-verify Verifikation aus (Default).\n";
print " --ip ADDR Diese IP verwenden statt externe IP abzufragen.\n";
print " --ipv6, -6 IPv6 (AAAA) verwenden statt IPv4 (A).\n";
print "\nKonfiguration über Umgebungsvariablen: ZONE, TARGETSUB, API_ID, EMAIL_NOTIFY, NS\n";
exit 0;
},
) or exit 1;
# Konfiguration (ENV mit Defaults)
my $ZONE = $ENV{ZONE} // 'liveconfig-bei-domainexpress.eu';
my $TARGETSUB = $ENV{TARGETSUB} // 'www3';
my $API_ID = $ENV{API_ID} // 'API_ID';
my $EMAIL_NOTIFY = $ENV{EMAIL_NOTIFY} // '';
my $NS = $ENV{NS} // 'ns1.hofmeirmedia.net';
my $ZONE_URL = "https://domainexpress.de:443/api/dns_zone/my_zone/$API_ID/";
my $IP_URL_V4 = 'http://v4.liveconfig-bei-domainexpress.eu/';
my $IP_URL_V6 = 'http://v6.liveconfig-bei-domainexpress.eu/';
# Abhängigkeit: dig für DNS-Abfragen (für TXT und autoritative A-Records).
# Wenn dig fehlt, wird auf den System-Resolver zurückgefallen.
my $HAVE_DIG = 1;
{
my $ok = system('dig -v >/dev/null 2>&1');
if ($ok != 0) {
$HAVE_DIG = 0;
warn "Hinweis: 'dig' nicht gefunden oder nicht ausführbar – verwende System-DNS.\n";
}
}
my $http = HTTP::Tiny->new( timeout => 30 );
# --- Debug-Helfer ---
sub _dbg {
return unless $DEBUG;
my ($msg) = @_;
chomp $msg;
print STDERR "[DEBUG] $msg\n";
}
# IP und Typ (A vs AAAA): per --ip (mit Auto-Erkennung) oder extern abfragen
my ($ip, $rtype, $dig_type);
if (length $opt_ip) {
$ip = $opt_ip;
$ip =~ s/^\s+|\s+$//g;
die "Fehler: Ungültige oder leere IP bei --ip.\n" if !length $ip;
if ($ip =~ /:/) {
$rtype = 'aaaa';
$dig_type = 'AAAA';
} else {
$rtype = 'a';
$dig_type = 'A';
}
_dbg("IP aus Option --ip: $ip ($rtype)");
} else {
if ($opt_ipv6) {
$ip = get_external_ip($IP_URL_V6);
die "Fehler: Konnte externe IPv6 nicht ermitteln.\n" if !length $ip;
$rtype = 'aaaa';
$dig_type = 'AAAA';
_dbg("Externe IPv6: $ip");
} else {
$ip = get_external_ip($IP_URL_V4);
die "Fehler: Konnte externe IPv4 nicht ermitteln.\n" if !length $ip;
$rtype = 'a';
$dig_type = 'A';
_dbg("Externe IP: $ip");
}
}
# Aktuellen Eintrag aus DNS holen (A oder AAAA)
# Kein Eintrag = undef/leer → Update durchführen und Eintrag anlegen
my $ip_now = $rtype eq 'aaaa' ? dig_aaaa("$TARGETSUB.$ZONE") : dig_a("$TARGETSUB.$ZONE");
if (defined $ip_now) {
$ip_now =~ s/\s+$//;
$ip_now = undef if $ip_now eq '';
}
_dbg(defined $ip_now ? "Aktueller DNS-$dig_type-Record für $TARGETSUB.$ZONE: $ip_now" : "Kein $dig_type-Record für $TARGETSUB.$ZONE (wird angelegt)");
if (defined $ip_now && $ip_now eq $ip) {
print "DNS bereits aktuell ($ip)\n";
exit 0;
}
print "Starte Update auf $ip für $TARGETSUB.$ZONE\n";
_dbg("ZONE_URL: $ZONE_URL");
# Zone abrufen
my $zone_text = http_get($ZONE_URL);
die "Fehler: Zone konnte nicht geladen werden.\n" if !length $zone_text;
_dbg("Zone-Länge vor Update: " . length($zone_text) . " Bytes");
# Zone bearbeiten: A- oder AAAA-Record für TARGETSUB
$zone_text = update_zone_ip_record($zone_text, $rtype, $TARGETSUB, $ip);
# last_change TXT setzen/aktualisieren
my $last_change_value = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime);
$zone_text = update_zone_txt_record($zone_text, 'last_change', $last_change_value);
# Optional: E-Mail-Benachrichtigung
if (length $EMAIL_NOTIFY && $zone_text !~ /^email::1\/notify:/m) {
$zone_text .= "email::1/notify: $EMAIL_NOTIFY\n";
}
# Zone zurückschicken
my $res = http_post($ZONE_URL, $zone_text);
if (!$res->{success}) {
print STDERR "Fehler beim POST: " . ($res->{content} // $res->{status}) . "\n";
exit 1;
}
_dbg("Serverantwort Status: $res->{status}") if defined $res->{status};
print $res->{content} . "\n" if length($res->{content} // '');
print "Update abgeschlossen.\n";
# Optional: Verifikation per DNS
if ($VERIFY_LOOP && $HAVE_DIG) {
my $max_wait = 300;
my $interval = 15;
my $waited = 0;
print "Prüfe, ob TXT last_change im DNS erscheint ...\n";
while ($waited < $max_wait) {
my $dns_txt = dig_txt("last_change.$ZONE");
$dns_txt =~ s/^"|"$//g;
if (defined $dns_txt && $dns_txt eq $last_change_value) {
print "Änderung im DNS bestätigt: last_change=$dns_txt\n";
exit 0;
}
sleep $interval;
$waited += $interval;
}
print "Warnung: last_change-TXT wurde nach ${max_wait}s noch nicht im DNS gesehen.\n";
}
elsif ($VERIFY_LOOP && !$HAVE_DIG) {
warn "Verifikation via TXT-Record ist aktiviert, aber 'dig' ist nicht verfügbar – überspringe Prüf-Schleife.\n";
}
# --- Hilfsfunktionen ---
sub get_external_ip {
my ($url) = @_;
$url //= $IP_URL_V4;
my $r = $http->get($url);
return '' unless $r->{success} && length($r->{content});
my $ip = $r->{content};
$ip =~ s/\s+$//;
return $ip;
}
sub dig_a {
my ($fqdn) = @_;
if ($HAVE_DIG) {
open my $fh, '-|', 'dig', '@' . $NS, '+short', 'A', $fqdn or return undef;
my $line = <$fh>;
close $fh;
return defined $line ? $line : undef;
}
my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname($fqdn);
return undef unless @addrs;
return inet_ntoa($addrs[0]);
}
sub dig_aaaa {
my ($fqdn) = @_;
return undef unless $HAVE_DIG;
open my $fh, '-|', 'dig', '@' . $NS, '+short', 'AAAA', $fqdn or return undef;
my $line = <$fh>;
close $fh;
return defined $line ? $line : undef;
}
sub dig_txt {
my ($fqdn) = @_;
# TXT-Records können wir nur mit 'dig' sauber auflösen.
return undef unless $HAVE_DIG;
open my $fh, '-|', 'dig', '@' . $NS, '+short', 'TXT', $fqdn or return undef;
my $line = <$fh>;
close $fh;
return defined $line ? $line : undef;
}
sub http_get {
my ($url) = @_;
my $r = $http->get($url);
return '' unless $r->{success};
return $r->{content} // '';
}
sub http_post {
my ($url, $body) = @_;
return $http->request('POST', $url, {
content => $body,
headers => { 'Content-Type' => 'application/octet-stream' },
});
}
# $rtype = 'a' oder 'aaaa' (Zonenformat: a:: bzw. aaaa::)
sub update_zone_ip_record {
my ($zone, $rtype, $source, $ip) = @_;
my $prefix = $rtype; # "a" oder "aaaa"
my $label = $rtype eq 'aaaa' ? 'AAAA' : 'A';
my $record_id;
if ($zone =~ /^\Q$prefix\E::(\d+)\/source: \Q$source\E\s*$/m) {
$record_id = $1;
}
if (defined $record_id) {
print "Gefundener bestehender $label-Record mit ID $record_id – aktualisiere target\n";
$zone =~ s/^(\Q$prefix\E::\Q$record_id\E\/target: ).*/$1$ip/m;
return $zone;
}
print "Kein bestehender $label-Record für $source gefunden – füge neuen Eintrag hinzu\n";
my $max_id = 0;
while ($zone =~ /^\Q$prefix\E::(\d+)\/source:/gm) { $max_id = $1 if $1 > $max_id; }
my $new_id = $max_id + 1;
$zone .= "${prefix}::${new_id}/source: $source\n${prefix}::${new_id}/target: $ip\n";
return $zone;
}
sub update_zone_txt_record {
my ($zone, $source, $target) = @_;
my $record_id;
if ($zone =~ /^txt::(\d+)\/source: \Q$source\E\s*$/m) {
$record_id = $1;
}
if (defined $record_id) {
print "Aktualisiere TXT-Record $source (ID $record_id) auf $target\n";
$zone =~ s/^(txt::\Q$record_id\E\/target: ).*/$1$target/m;
return $zone;
}
print "Füge neuen TXT-Record $source mit Wert $target hinzu\n";
my $max_id = 0;
while ($zone =~ /^txt::(\d+)\/source:/gm) { $max_id = $1 if $1 > $max_id; }
my $new_id = $max_id + 1;
$zone .= "txt::${new_id}/source: $source\ntxt::${new_id}/target: $target\n";
return $zone;
}