Custom Dynamic DNS with UniFi Dream Machine Pro

Will Dixon
6 min readAug 13, 2020

--

I recently bought a UniFi Dream Machine Pro (UDM Pro). I did a ton of research before getting it and saw Dynamic DNS (DDNS) support. But it only supported a hand full of DDNS providers. My domains are in AWS Route53, and I did not want to pay premium pricing for the supported DDNS providers. So I embarked on a journey to create a personal provider to update a subdomain hosted in AWS Route53.

I first need to figure out how I am going to do it. What does the UDM Pro do? What do the other DDNS providers have that allows the UDM Pro to update them? Any specific configurations I need to set on the UDM Pro?

How does UDM Pro notify a DDNS Provider?

To figure this out, I had to go through the logs of my UDM Pro and see what happens when I set up a dummy DDNS Provider. I gave it fake credentials, so it would fail and spit something out to the logs. It turns out that the UDM Pro uses Internet Automated Dynamic DNS Client (Inadyn), which is an Open Source tool!

Inadyn has excellent documentation and even supports more providers than UniFi OS (the software that UDM Pro runs) allows. But since Route53 is just a DNS provider without a native way to dynamically update records, I would still have to roll out a custom solution. Luckily, Inadyn supported custom configuration, which means I could set up any API I wanted, and I could get Inadyn to communicate with it appropriately!

But I know there has to be a standard DDNS API somewhere. I went through some of the supported providers and found that https://dyn.com had some documentation around their APIs (https://help.dyn.com/remote-access-api/). It went through the whole cycle and what the expected response codes were!

So, with all that figured out, I need to write a service that accepts and appropriately handles the following HTTP request:

GET /nic/update?hostname=home.mydomain.net&myip=192.168.0.1 HTTP/1.0
Host: ddns.mydomain.net
Authorization: Basic base-64-authorization
User-Agent: Company - Device - Version Number

And a response that looks like:

HTTP/1.0 200 OK
Content-Length: 4
Content-Type: text/plain
good

I will be handling other error code responses to ensure that the Inadyn handles the response appropriately.

Update UDM Pro with Custom DDNS Provider

Now I need to figure out how to configure the UDM Pro to use a custom DDNS provider.

I quickly set up an EC2 instance and wrote a simple Go service that listened to port 443. All it did was print out information about the request then respond with a status of good. I pointed my ddns subdomain to the EC2 instance and set up TLS with Let’s Encrypt (Inadyn supports HTTPS by default). If you are following along, don’t forget to update your security groups to allow port 80 (for Let’s Encrypt) and 443 (for Inadyn running on UDM Pro) on all IP addresses.

package mainimport (
"log"
"net/http"
)
func main() {
err := http.ListenAndServeTLS(":443", "/etc/letsencrypt/live/ddns.mydomain.net/fullchain.pem", "/etc/letsencrypt/live/ddns.mydomain.net/privkey.pem", &handler{})
if err != nil {
log.Fatal(err)
}
}
type handler struct{}func (*handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
log.Println(req.URL.String())
auth, found := req.Header["Authorization"]
if found {
log.Println(auth)
}
agent, found := req.Header["User-Agent"]
if found {
log.Println(agent)
}
h := resp.Header()
h.Set("Content-Type", "text/plain")
resp.WriteHeader(http.StatusOK)
resp.Write([]byte("good"))
}

I logged into my UDM Pro and set up the setting how I thought they should be for that server (set Service to dyndns, Server to ddns.mydomain.net, and filling the rest in as I would any other provider). When I hit apply, my Go application didn’t return anything. I went digging through UDM Pro logs and found:

Aug 13 01:46:32 Home user.warn inadyn[18541]: Fatal error in DDNS server response:
Aug 13 01:46:32 Home user.warn inadyn[18541]: [400 Bad Request] 400 Bad Request
Aug 13 01:46:32 Home user.warn inadyn[18541]: Error response from DDNS server, ignoring ...

I felt like there should be more output, so I looked up a way to manually run Inadyn without waiting for my IP address to change. Notice: configuration file for UDM Pro is different than the default location for Inadyn.

/usr/sbin/inadyn -n -s -C -f /run/inadyn.conf -1 -l debug --foreground

The above showed me that Inadyn made this request:

GET home.mydomain.net HTTP/1.0
Host: ddns.mydomain.net
Authorization: Basic dXNlcjpwYXNzd29yZA==
User-Agent: inadyn/2.5 https://github.com/troglobit/inadyn/issues

Notice how the home.mydomain.net portion is not a valid path. It’s missing the first forward-slash. Digging around in Ubiquiti forums and turns out this is a hidden feature. UniFi OS is supposed to save the Inadyn file a certain way but does so in strange ways. The following was the configuration file UniFi OS created (/run/inadyn.conf).

#
# Generated automatically by ubios-udapi-server
#
iface = eth8
custom ddns.mydomain.net {
hostname = "home.mydomain.net"
username = "user"
password = "password"
ddns-server = "ddns.mydomain.net"
}

Even though I specified a Service of dyndns, it knows it is a custom DDNS provider because I set the Server field! I went through Inadyn documentation again. It turns out, with the above configuration, it appends hostname to ddns-server. What I want instead is for me to be able to set the query parameters of the URL. Inadyn has a way to do that! The following is the line I want to put in the configuration file.

ddns-path = "/nic/update?hostname=%h&myip=%i"

I don’t want to modify it directly as firmware updates will overwrite the file! So I appended /nic/update?hostname=%h&myip=%i to the Server configuration and tried again.

GET nic/update?hostname=home.mydomain.net&myip=192.168.0.1home.mydomain.net HTTP/1.0
Host: ddns.mydomain.net
Authorization: Basic dXNlcjpwYXNzd29yZA==
User-Agent: inadyn/2.5 https://github.com/troglobit/inadyn/issues

So close! It appears to be still appending the hostname field to the path and the path needs to have a forward-slash at the beginning for it to be valid. I did try some finagling of escaping the forward-slash to no avail. But a user on the Ubiquiti forum found a solution. It turns out that UniFi OS is trimming off the forward-slashes. So if you put a dummy one first, then “escape” the second one, you should get the expected results.

ddns.mydomain.net/\/nic/update?hostname=%h&myip=%i

This is not needed anymore with an update Ubiquiti pushed out recently! More info in the Update section below.

Kind of…

#
# Generated automatically by ubios-udapi-server
#
iface = eth8
custom ddns.mydomain.net {
hostname = "home.mydomain.net"
username = "user"
password = "password"
ddns-server = "ddns.mydomain.net"
ddns-path = "\/nic/update?hostname=%h&myip=%i"
}

Even though the back-slash is in the configuration, Inadyn seems to handle it beautifully, and I got a response from my dummy Go service!

2020/08/13 02:23:36 /nic/update?hostname=home.mydomain.net&myip=192.168.0.1
2020/08/13 02:23:36 [Basic dXNlcjpwYXNzd29yZA==]
2020/08/13 02:23:36 [inadyn/2.5 https://github.com/troglobit/inadyn/issues]

Conclusion

UDM Pro Dynamic DNS Settings
Dynamic DNS Settings
  1. It doesn’t matter what Service is set to as UDM Pro will create a custom Inadyn configuration if Server is configured.
  2. Server setting is very particular in how it is configured. ddns.mydomain.net/\/nic/update?hostname=%h&myip=%i
  3. There is a very useful command for running inadyn manually while trying to develop something. /usr/sbin/inadyn -n -s -C -f /run/inadyn.conf -1 -l debug — foreground

While digging around, I did find the need to clear Inadyn’s cache. It caches the last successful update of an IP address, so it doesn’t make too many calls. The following locations are the directories in which Inadyn will cache those IPs.

  • /root/.inadyn — created when running the manual command
  • /.inadyn — created when the UDM Pro service runs Inadyn

What’s Next?

After all that, I think it is time to define how my DDNS provider will look. I believe using AWS’s API Gateway and Lambda functions will be the best course of action. And because I want to have fun, I may write the Lambda functions in Rust!

Disclaimer

I replaced the domain that I will be using with mydomain.net, which I do not own. I also replaced all IPs that would have shown my public IP with 192.168.0.1 .

The above information is with my UDM Pro. It is running Controller Version 5.13.30 and Firmware Version 1.7.2.2620.

Update

A recent update to the UDM Pro Firmware or Controller “broke” my DDNS setup. I went through the logs (see code section below) and AWS was blocking my requests. I read that an improper URL path could cause the error. So I removed the funky \/ in front of nic and it started working again! So Ubiquiti has resolved this issue! If a Ubiquiti employee is reading this, very much appreciated, and lots of thanks! My UDMP Controller Version is 6.0.41 and the Firmware Version is 1.8.3.

The error message in logs before I fixed the URL:

"{"message":"'{My Token}' not a valid key=value pair (missing equal-sign) in Authorization header: 'Basic {My Token}'."}"

The stack overflow question that led me to try and remove the \/ from the path https://stackoverflow.com/questions/57168148/unable-to-resolve-not-a-valid-key-value-pair-missing-equal-sign-in-authoriza.

The controller version that fixed it https://community.ui.com/releases/UniFi-Network-Controller-6-0-26/7298f408-d9bf-48d7-b92c-a29c5b73b769.

--

--

Will Dixon

Senior Site Reliability Engineer and technology hobbyist