La Vita è Bear

Combine UniFi API with dynamic DNS client

I had been using Google/Nest Wifi routers since the original OnHub days. Later upgraded to Google Wifi, then Nest Wifi Pro. While there are a lot of good things from those systems, there are also some really annoying things. The original OnHub had broken/non-existing NAT loopback implementation, which took them several months to finally fix. Then when Nest Wifi Pro is released NAT loopback is broken again for several months until they finally fixed it; With Nest Wifi Pro they also switched from the Google Wifi android app to Google Home app, which often gives you lies (a mesh point is down but the app says everything is fine), stale info, or every action takes several seconds to refresh. So, earlier this year, we finally had enough and decided to move to other wifi routers. I asked my colleagues for suggestions, and decided to buy into Ubiquity’s UniFi system. In particular, we bought a Dream Router 7 for the main router, and an Express 7 for meshing and providing wired connection to PS5 Pro, because PS5 Pro’s wifi chip is not great.

Among several features it provided, one is to set up VPN clients on the router level and auto route traffic from the devices to the VPN client. This also comes with a challenge, because I run a Linux server at home (an Intel NUC box), with dynamic DNS client to run in an hourly cron job to make sure I have the domain pointing to the correct IP (as I don’t have static IP). The dynamic DNS client just gets the “correct” IP by getting its egress IP from the API, so if the request is routed via VPN, then it won’t be able to get the correct IP address to set.

Of course I can set routing rules to make sure those requests don’t go through VPN. But domain based routing rules are brittle with DNS security used, and IP based routing rules are infeasible because that requires me to keep an up-to-date list of all the possible IPs the API endpoint can resolve to.

So instead, I turned to another new feature provided by UniFi: their REST API can tell me what my external IP is. I just need to generate an API key from site manager, then this can be done via a simple curl + jq pipe:

apikey="..."

ip=$(curl --header "X-API-Key: ${apikey}" --header 'Accept: application/json' "https://api.ui.com/v1/hosts" | jq -r '.data.[0].reportedState.ip')

And then I can feed ${ip} to my dynamic DNS client to use.

But things get complicated when you want to use that in cron jobs. In cron jobs, you don’t want to output anything to stdout/stderr when things are OK, as anything output will be treated as an error and send a mail to admin, and I don’t need hourly mails. In cron job you also want it to fail early if things go wrong, so you don’t set a wrong IP to your DNS.

With all those in mind, the simple one line piping command needs to be expanded into something like:

apikey="..."

curlout=$(curl --silent --show-error --fail --header "X-API-Key: ${apikey}" --header 'Accept: application/json' "https://api.ui.com/v1/hosts" 2>&1)

if [ $? -ne 0 ]; then
  echo "$curlout"
  exit 1
fi

ip=$(echo "$curlout" | jq -r '.data.[0].reportedState.ip')

At that point, it no longer makes sense to pipe curl + jq. It makes more sense to implement that in the dyndns client itself. This way, I also can handle more logic like filtering through the IPs returned by the API to find the first public v4 IP.

In the end, I just add the new unifi api key to my cron job as another arg:

exec /path/to/ddporkbun --apikey="${apikey}" --secretapikey="${seckey}" --unifi-apikey="${unifikey}" --domain="mydomain.com" --subdomain="dyndns" --log-level=ERROR
#English #go #tech #porkbun #unifi