DynDNS mit Azure

Mittels Azure DNS und Azure Functions lässt sich mit wenig Aufwand ein eigener DynDNS-Dienst erstellen. Dieser Beitrag beschreibt das Vorgehen und liefert den Quellcode dazu.

Azure DNS bietet ein beinahe kostenloses und komfortabel verwaltbares Hosting von DNS-Zonen. Mittels Azure Functions lassen sich Funktionen "serverless", also ohne Betrieb eines gehosteten Servers ausführen.

Die Idee ist folgende: von einem Server oder Router an einem Internetanschluss mit dynamischer IP-Adresse wird regelmässig die Funktion aufgerufen. Diese prüft, ob sich die IP-Adresse geändert hat und aktualisiert wenn nötig den Eintrag im DNS.

Der Dienst soll möglichst einfach sein und ohne Registration auskommen. Deshalb wird die Authentifizierung über einen Hashwert gelöst, welcher aus dem Hostnamen und einem Passwort besteht. Kennt man den Update-URL für Host X, kann man damit nicht Host Y updaten.

Vorbereitung

  • Azure-Konto erstellen
  • DNS-Zone in Azure erstellen: hier würde ich aus Sicherheitsgründen eine neue Domain oder Subdomain verwenden, zum Beispiel "dyn.domain.ch"
  • Azure-DNS-Server in übergeordneter Domain eintragen
  • Im Azure AD eine neue App erstellen und diese der Rolle "DNS Zone Contributor" hinzufügen. Ein neues Client Secret erstellen. Dieses, sowie die Client ID und die Tenant ID werden später benötigt.

Erstellung der Funktion

Die Funktion kann entweder im Azure Portal oder in Visual Studio erstellt werden. Ich bevorzuge die Entwicklung mit Visual Studio, da man den Code so bequem debuggen kann. Die Funktion muss ohne Authentifizierung über einen HTTP-Trigger aufgerufen werden können.

Der Sourcecode:

public static class dnsUpdater
    {
        [FunctionName("dnsUpdater")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("Starte Verarbeitung...");

            string hostname = req.Query["hostname"];
            string key = req.Query["key"];
            string newIp = (req.Headers["X-Forwarded-For"].FirstOrDefault() ?? "").Split(new char[] { ':' }).FirstOrDefault();

            // für lokalen Test (ohne Reverse Proxy)
            if (String.IsNullOrEmpty(newIp))
            {
                newIp = req.HttpContext.Connection.RemoteIpAddress.ToString();
            }

            // Parameter prüfen
            if (String.IsNullOrEmpty(hostname) || String.IsNullOrEmpty(key))
            {
                log.LogError($"Hostname oder Key fehlt, IP {newIp}!", new { hostname, key });
                return new BadRequestObjectResult($"Hostname oder Key fehlt, IP {newIp}!");
            }

            hostname = hostname.ToLower();
            key = key.ToLower();

            // Key prüfen
            const string pw = "PWfuerUpdate";
            string hash = CreateMD5(hostname + ":" + pw);

            if(key != hash.ToLower())
            {
                log.LogError($"Key ist falsch, IP {newIp}!", new { hostname, key });
                return new BadRequestObjectResult($"Key ist falsch, IP {newIp}!");
            }

            // über eine DNS-Abfrage testen, ob IP-Adresse geändert hat und angepasst werden muss
            const string domain = "dyn.domain.ch";
            string currentIp = "";

            try
            {
                currentIp = Dns.GetHostEntry(hostname + "." + domain).AddressList.FirstOrDefault().ToString();
            }
            catch(SocketException e)
            {
                log.LogInformation($"Host {hostname} existiert noch nicht!");
            }

            if (newIp == currentIp)
            {
                log.LogInformation($"IP hat nicht geändert, ist immer noch {newIp}");
                return new OkObjectResult($"IP ist unverändert {newIp}");
            }

            // IP muss geändert bzw. Hosteintrag erstellt werden
            string clientId = "Client-ID";
            string clientSecret = "Client-Secret";
            string tenantId = "Tenant-ID";
            string subscriptionID = "Subscription-ID (von DNS-Zone)";

            var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(clientId, clientSecret, tenantId, AzureEnvironment.AzureGlobalCloud);
            var azure = Azure
            .Configure()
            .WithLogLevel(HttpLoggingDelegatingHandler.Level.BodyAndHeaders)
            .Authenticate(credentials)
            .WithSubscription(subscriptionID);
            var rootDnsZone = azure.DnsZones.GetByResourceGroup("ressourcengruppe", domain);

            rootDnsZone = rootDnsZone.Update()
            .DefineARecordSet(hostname)
            .WithIPv4Address(newIp)
            .WithTimeToLive(300)
            .Attach()
            .Apply();

            return new OkObjectResult($"IP erfolgreich geändert in: {newIp}");
        }

        public static string CreateMD5(string input)
        {
            // Use input string to calculate MD5 hash
            using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
            {
                byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
                byte[] hashBytes = md5.ComputeHash(inputBytes);

                // Convert the byte array to hexadecimal string
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < hashBytes.Length; i++)
                {
                    sb.Append(hashBytes[i].ToString("X2"));
                }
                return sb.ToString();
            }
        }
    }

Der Aufruf erfolgt über https://funktionsname.azurewebsites.net/api/dnsUpdater?hostname=test&key=48104331b7b009c6d354fc0b900669a5.

Der Key berechnet sich wie folgt: MD5(hostname + ":" + pw). MD5-Hashes lassen sich zum Beispiel auf https://www.md5hashgenerator.com erstellen.

Ruft man den Link im Browser auf, erscheint die Meldung, ob der DNS-Eintrag aktualisiert werden musste oder nicht. (Aufgrund des DNS-Caches kann bei kurzen Intervallen temporär "Eintrag wurde geändert" angezeigt werden, obwohl der Eintrag nicht geändert werden musste.)

Standardmässig ist die Funktion nur per HTTPS erreichbar. Verwendet man ein Gerät, welches damit nicht umgehen kann, kann man in den "Platform features" auch HTTP erlauben.

Mit der Aufgabenplanung von Windows kann man den Updater über

powershell.exe -command "Invoke-WebRequest 'https://funktionsname.azurewebsites.net/api/dnsUpdater?hostname=host&amp;key=key'"

starten.

Quellen
- DNS-Management über Azure Management Libraries: https://henrikmotzkus.de/azure-functions-nutzen-fuer-eigenen-dyndns-dienst/
- MD5-Funktion: https://stackoverflow.com/questions/11454004/calculate-a-md5-hash-from-a-string

Ein Gedanke zu „DynDNS mit Azure

  1. Pingback: DynDNS einrichten auf ZyWALL - nexcon ag

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.