Implementing a Dynamic DNS utility in Rust

So I recently finished a small Rust project: ddns is a utility for implementing dynamic DNS using the cloudflare API. I wanted to write this post to document my process and learnings before moving on.

The Dream

So this project started because I wanted to have ssh access to my home PC from outside my home LAN. I do work with computer graphics, and since my home PC has a much stronger GPU than my laptop, it's nice to be able to run tests and see the results even when I'm working from the cafe down the street.

In journey to achieve this use-case, my first attempt was to access my home PC via IP address. This required figuring out port forwarding on my home router in order to make my PC available from the outside world. And this worked just fine, however there was a catch: whenever I restarted my machine, it would be assigned a new IP address. Logistically this was a pain, so I started to explore the possibility of connecting my home PC to proper domain name, so that even if the IP address changed, the computer would still be available.

Dynamic DNS

While researching this option, I learned about dynamic DNS. The concept is simple: for a computer with an IP address which changes, the DNS records pointing to that computer need to be changed to reflect the new address, so the domain resolution process will continue to point to that machine.

There are some commercial services out there to accomplish this which even have free tiers, but their buisiness model appears to be to make the free tier annoying enough to use that you will upgrade to a paid service level, so I decided to look elsewhere.

So I also looked into duckdns, which is a free service for dynamic DNS. It's actually a really nice offering, and the only real drawback is that your domain will be a subdomain of I.e. you cannot bring your own domain for this service. I might have stuck with duckdns anyway, but I ran into some problems due to the fact that my computer only seems to get an ipv6 address (no ipv4), and since I really did want my own domain due to my perfectionist tendancies I decided not to put much effort into debugging this issue so I looked elsewhere.

Route 53

That's what brought me to this excellent article about using route 53 for dynamic DNS. Basically the concept of this approach is to use AWS's DNS service to host your domain, and then you can use the API to update the DNS records periodically to stay in sync with the changing IP address. I have quite a bit of experience with AWS so this seemed like an easy way to go.

I followed the approach in the article, and I had to make a couple of changes: first of all, my domain is hosted on cloudflare, so I had to add NS records on cloudflare to delegate the DNS for the subdomain I wanted to use for my home pc to AWS's nameservers. Also I had to make some modifications to the shell script shown in the article to use ipv6 rather than ipv4.

So this actually worked pretty well. I was able to get dynamic DNS working in this manner, and I set up systemd to run the script every time my computer started up to automate keeping the DNS records in sync. And in the process I learned a lot about how DNS works, and how to use tools like dig to debug DNS issues.

But it wasn't perfect. The way the shell script is written, it caches the ip address, and skips the DNS update if the address doesn't change. The problem is, errors are not handled exhaustively, which meant there were cases where the ip address would be cached, but the DNS records would not be updated successfully. This meant the DNS would be out of sync, and what's worse is the next time the update script would run, it would detect that no update was needed, so you would end up in this no-man's land where the update just wouldn't happen until the address changed again. I considered fixing the script, but it seemed like the complexity of the problem had reached the level where bash might be under-powered and the facilities of a proper programming language would be appreciated.

Rewrite it in Rust

So now that the problem was well understood, I decided to implement the solution in Rust. This was kind of a natural choice, because I've been looking for more opportuinities to experiment with Rust for real-world problems, and this was exactly the kind of small, well-defined use-case which I could implement in a few days and learn a few things along the way. Also, for this kind of utility which you sort of just want to get it working and then just forget about it, Rust's features with respect to safety and correctness, as well as the excellent cross-platform toolchain gave me the confidence that I would be able to use this tool for years to come with whatever distro would I so choose.

So I got to work. Originally I was going to stick with Route53 for DNS, but then I realized that Cloudflare also has an API - so I could simplify things, remove the AWS from the process, and just update the AAAA records in Cloudflare directly. Cool. So I deleted my hosted zone in Route53 and my NS records in Cloudflare and started persuing this path.

Overall the experience of developing a tool like this in Rust was a good one. I decided to do the entire project over ssh using the terminal only, editing in vim. It's the first time I've done a project of this size using vim alone, and I definitely improved my speed and competancy using the tool. Also I found that Rust is a good language for this type of workflow, thanks to the excellent compiler error messages. It became quite natural to open a file and jump straight to the line cited in the error.

I would say the one unexpectedly tricky part of the project was getting TLS to work on the cloudflare api requests. I was a bit surprised to find that Rust doesn't have an official http client as part of the standard library, as I have come to expect from modern languages. So I looked into options, and reqwest seems to be quite popular, but I wanted to avoid it because it seemed a bit overkill for my very simple use-case and came with a lot of dependencies. So then I tried using hyper, which reqwest is built on top of, but it turns out hyper basically requires a runtime like tokio in order to do https, which seemed like a lot of complexity to add to my little CLI which does one or two requests in a synchronous manner. So in the end I went with ureq which is more purpose-built for this scenario. I guess it is fine that those alternatives exist, but I have to say I kind of missed having one "blessed" http client implementation which I could go to rather than hunting around through various implementations to find the right one.

Anyway, my dynamic DNS is working now. It was a fun project, I learned a lot about DNS in the process, and I look forward to making more tools in Rust in the future.

Let's work together! I'm a freelance engineer, and I specialize in mobile applications, computer graphics, and image processing. I'm always looking for interesting projects.

[email protected]