DDNS with OpenWRT and Azure
The weirdly specific configuration that someone else might actually use.
This configuration may be of use to you if you have the following:
Your domain hosted by Microsoft Azure
A router running OpenWRT³
(I just upgraded mine to the seeed Raspberry Pi-based mini router, to make better use of my gigabit Internet connection and so that I could terminate my Tailscale mesh VPN there rather than internally, among other minor considerations.)
A burning desire to have your IPv4 and IPv6 WAN addresses dynamically updated, lest your ISP one day crazily allocate you different ones1, and to have them updated in your proper domain records, not farming them out to a generic dynamic DNS provider.
Or, I suppose, if you just enjoy the pure technical curiosity of the thing.
Let’s get started.
Prerequisites
A router running OpenWRT³
A domain hosted at Azure. I’m not going to tell you how to set that up, because they have a perfectly good tutorial already. So before you go any further, make sure you can hop onto the Azure portal and see both your zone:
And within it, the A and/or AAAA records you want to update. (Not strictly necessary, but checking that you got the static records right before you make them dynamic might save you debugging time later.)
Visual Studio Code, complete with the relevant extensions for working on Azure Functions. (Those would be Azure Account, Azure Resources, and - you guessed it - Azure Functions.)
Got all of those handy? Now we’re ready to go.
How To Do The Thing
Okay. Here’s how this works.
There is a dynamic DNS updater service built into OpenWRT. (If you don’t have them on your router, you can install them thus:
root@OpenWrt:~# opkg install curl ddns-scripts luci-app-ddns
The former package is the service, and the latter the ability to configure it through the LuCI web interface. If you want, you can always skip the latter and do all the configuration through uci directly. I didn’t want.)
This service can be configured to use custom dynamic DNS providers, which it does by making a request to a URL with the necessary information for the update. We’re going to point that URL to an Azure Function we’ve created, which will in turn run the necessary PowerShell commands, in the Azure context, to update our DNS entry.
Well, actually, we’re going to do that twice, once for IPv4 and once for IPv6.
Actually Doing The Thing
Creating the Functions
First off, you need to create the functions locally in Visual Studio Code. I’m not going to give you a great deal of detail here, because once again, Microsoft already have a good tutorial on creating PowerShell functions. I’m just going to take you through the relevant bits.
So go ahead and follow the “Create your local project” instructions in that tutorial. You’ll need to pick PowerShell as the language and HTTP Trigger as the template, as it says, but give it an appropriate name (like, say, UpdateMyIPv4Address), and when it asks for an authorization level, pick function. After all, you don’t want just anyone updating your DNS zone, do you?
Putting Meat on the Bones
Now we need to make the functions do what we want. Before I start walking you through this section, I’ll tell you that the whole of my function code to do this is available on GitHub at cerebrate/ddns, so when it comes down to it, you just need to make the template you’ve got look like that.But that’s a bit blunt, so here’s the edit-by-edit walkthrough.
Background Foo
We start with some of the background foo needed to make the functions work. First up, modify the requirements.psd1 file in the root to look like this:
@{
'Az.Accounts' = '2.*'
'Az.Dns' = '1.*'
}
This ensures the function has access to the libraries it needs to identify itself to Azure and to manipulate your DNS zone.
Next, put this in profile.ps1, which gets executed every time your function app starts up:
Connect-AzAccount -Identity
That makes sure it’s logged on to Azure, and thus that the functions contained within will be able to work.
Into the Functions: UpdateMyIPv4Address
Now head into the directory where the function itself is. You’ve got two files in there. The first is function.json, which defines how the function is to be called and will run. It should look like this (see here); all very simple: your basic authenticated HTTP trigger, accepting GET or POST and returning a normal response.
And then there’s run.ps1, which does the actual update. See it here.
So, for a breakdown of what it does, lines 3-24 are all about getting the parameters. The script takes three, name, zone, and reqIP, respectively the name of the A record you want to update (say, “router”); the name of the DNS zone to update (say, “example.com”), and the IP address you want to place in that record (say, “192.168.0.1”). These lines get it from the query string (so you can handle everything in the URL), and if they don’t find it there, look in the request body for it.
Assuming it finds everything it needs (and so doesn’t simply return a bad request), it proceeds to look up the current record, if there is one (line 28). If there is one, it then checks the current IP in the record against the one you requested (lines 36-38). If it’s the same, it doesn’t bother updating it (lines 46-48), but if it’s different, modifies it to the new IP (lines 39-44). If there isn’t one, on the other hand, it will create a new A record with the given name and the new IP address (lines 51-55).
And finally it will return the result to the caller (lines 63-66).
You can use this code pretty much as is. There are two things you may need to change to match your side. One is the various -ResourceGroupName arguments to various commands (I keep all my DNS zones in the Standard resource group in my Azure account, but you may not), and the other is the -Ttl argument to New-AzDnsRecordSet, since while that is more or less standard, you may want a different TTL for your A record if your dynamic IP is in the habit of changing a lot.
Into the Functions: UpdateMyIPv6Address
See here. It works in precisely the same way as the IPv4 one except for referencing AAAA records rather than A records, right down to the line numbers, so rather than walk you through that again, see above.
If you want to do both, create a second function in your app, in Code, just like you did above, and add the function in that.
Creating the Function App
The next step is creating the function app, and giving it the access it needs to your DNS zone. The tutorial has you do this in Code, but I prefer to do it in the Azure portal. Either way, the procedure is much the same: pick your subscription, give it a name (which will become the hostname at which your functions are hosted), select a runtime stack (you want PowerShell Core), and a location for the resource (if you haven’t already got resources somewhere, pick the one nearest you).
Depending on your choices throughout this process, you will end up with various auxiliary resources to the function app. An Azure Storage account (which it does need in order to run), possibly an Application Insights instance connected to it to track usage (I decided I didn’t need that, you probably don’t, and if you need to look at the logs for debugging you can look at the filesystem logs), and an App Service plan, which defines the underlying host for your function app2.
Deploy the Function App
Go ahead. In Visual Studio code, in the relevant chunk of the sidebar, go ahead and hit the deploy button.
That one, right there.
You should now see your function app and its functions in the Azure portal, looking rather like this when expanded:
But you can’t run it quite yet. Or you could, but it would fail because it doesn’t yet have permission to update your DNS zone. So the next thing you need to do is give your function app a managed identity. If you look at your function app in the Azure portal and scroll down a bit, you’ll see an option for identity, and on the page that brings up, a switch under “System assigned”. Go ahead, flip that switch on and apply your changes.
Then you need to take that identity and give it permission to modify your DNS zone. To do this, head over in the portal to your DNS zone, and select “Access control (IAM)”, and click the “Grant access to this resource” button.
The role you wish to add is DNS Zone Contributor, and then you can select the managed identity you wish to add - i.e., that corresponding to the function app you just created. Thus:
Testing!
Once this has done, you can test the function app by heading over to its page in the portal, selecting a specific function from the Functions list, and then picking Code + Test, and using the Test/Run button. Doing so will open a window in which you can select the parameters you want to test your function with. For example:
Go ahead and test it a couple of times. (If you have both IPv4 and IPv6 functions, be sure to test both.) You can flip back to the DNS zone to see the address change when the function runs. If you have any problems, you can check the logs by selecting the “App Insights Logs” button underneath the code and flipping it over to “Filesystem Logs”.
You Are Functional
Now you have Azure functions which will do the updates for your dynamic DNS.
Before you leave the Azure portal, go to each function’s Code + Test page, and use the “Get function URL” button. Make sure that the “Key” field on the left is set to “Function key / default”, and then copy the URL on the right. It should look something like this:
https://yourdynamicdns.azurewebsites.net/api/UpdateMyIPv4Address?code=a-lengthy-encoded-string-here
That encoded string is the key that authenticates the calls you make to the function. We’ll need that, and the rest of the URL, later.
Configuring The Router
And now, hop over to your OpenWRT router3, to the configuration page for the Dynamic DNS service (under Services on the LuCI left menu). It should look something like this:
Delete whatever services are there already, whether they’re running or not, unless you have something specific in mind for them. We’re starting from a clean slate.
Go to create a new service, give it a suitable name (indicating whether it’s v4 or v6, for a start), select whether you want to use IPv4 or IPv6, and set the DDNS service provider to “— custom —”. Now, here’s how you fill out the the fields; leave the rest blank or at their defaults.
On the Basic Settings page:
Check the Enabled checkbox, so the service will run.
Under Lookup Hostname, enter the hostname you are updating. This setting lets the service check on the current address, so that it won’t try to update it if it’s already correct4.
Set IP address version to the one you want to send. If you’re going to update both IPv4 and IPv6 addresses, you’re going to use two services, as you can see in the screenshot above, each calling one of the two functions.
DDNS Service provider should already be “— custom —‘‘.
You’ll need to modify the URL of the function you made a note of above. The URL you have,
https://yourdynamicdns.azurewebsites.net/api/UpdateMyIPv4Address?code=a-lengthy-encoded-string-here
needs to be modified to include the relevant fields for the update. The above, for example, should be sufficed thus and placed in the Custom update-URL field:
https://yourdynamicdns.azurewebsites.net/api/UpdateMyIPv4Address?code=a-lengthy-encoded-string-here&name=[USERNAME]&zone=[DOMAIN]&reqIP=[IP]
Don’t put the code string in the Password field; the URL-encoding of passwords can muck it up. We’re also going to misuse a couple of other fields to fit our format.
Domain should be set to the DNS zone you wish to update, without the record (host) name, say, “example.com”.
Username should be set to the record (host) name, say, “router”.
Password isn’t used, but the UI expects some text here. “dummy” will do.
On the Advanced Settings page:
IP address source should be “Network”.
Network should be the WAN network of your router; usually wan.
DNS-Server should be set to one of the nameservers hosting your domain (i.e., the ones in the NS records that you configure with your domain name provider). Since you’re hosting it on Azure, this will be something like “ns1-04.azure-dns.com”. Don’t use your local nameserver for this; this is used to check the IP that is registered, and it’s only the domain hosting (Azure) servers that know this accurately.
On the Timer Settings page, there’s nothing you have to change, but I have expanded the Force Interval/Force Unit settings to 1 day. These define the number of times that the script will call the function even if your address hasn’t changed.
While the usual use for this is to deal with dynamic DNS providers that delete your records if they aren’t updated, this isn’t the case for Azure, so it is probably not needed. I use 1 day currently to make sure that I’m not caught out by any glitches, maintenance, or downtime that prevents a proper update when it does change; as I shake this new setup down, I’ll probably lengthen it further, to a week, and ultimately to only-run-on-change.
Save your configurations, start your services, and you should see the registered IPs and update times change before your very eyes!
And That’s It!
You now have your very own Azure-based dynamic DNS service (running for a couple of cents a month, most likely), and a router configured to use it.
If you happen to have a configuration like mine, I hope you find it useful. If you don’t and you’re still reading - well, I hope at least you weren’t bored.
Before anyone points it out: yes, the IPv6 address of your router is not all that useful because you’re not using NAT, and what matters is the IPv6 address of the actual host behind it. This is true so far as it goes - but as it turns out, the seeed Mini Router has plenty of space capacity, so I also run the reverse proxy server that handles things like, for example, Home Assistant and Plex on it.
If asked, although it should be the default, make sure you pick a Consumption (Serverless) app service plan. While there is a certain glorious extravagance in dedicating an entire server solely to your DDNS updates, the expense is less glorious.
If you don’t have one of those, you could always adapt this to be called in some other way, y’know?
There’s a caveat I’ll get to in a minute, but for now - yes, we implemented this check in the function, too, but it’s better if we don’t call the function if we can already know we don’t have to. That way we don’t wake up the Azure server and burn billable cycles.