Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remember current public IP and hit Hetzner API endpoint only if it changes #37

Open
shuuryou opened this issue Oct 28, 2023 · 6 comments

Comments

@shuuryou
Copy link

It might be a good idea not to hit the Hetzner DNS API every five or so minutes when your script is used as part of a cron job. Since you're getting the IP addresses anyway, maybe consider refactoring the script a little bit so it remembers the last public IP address, compares it with the current one, and only invokes any part of Hetzner's DNS API if there is a change.

I had to roll my own DynDNS updater for Namecheap years ago and here's how I achieved the above:

IP=$(curl -sS -A "$AGENT" "https://api.ipify.org")

if [ -z "${IP}" ]; then
	echo "No public IP address at all. Internet down? Not doing anything."
	exit 1
fi

if [ ! -f /tmp/ddnslastip ]
then
	OLDIP="unknown"
else
	OLDIP=$(</tmp/ddnslastip)
fi

printf "The last public IP address was \"%s\" and the current public IP address is \"%s\".\n" "$OLDIP" "$IP"

if [ "$OLDIP" = "$IP" ]; then
	echo "Public IP address has not changed. Nothing to do."
	exit 0
fi

echo "$IP" > /tmp/ddnslastip

# ... perform the update ...

This needs to be updated slightly for IPv6 but I'm sure you get the general idea. Also "api.ipify.org" doesn't do any user agent sniffing (Hetzner's IP address service does to decide whether to return a website or just the IP as plain text). If you switch to it, you can get rid of the pipe to grep.

@Giga-Pudding
Copy link

Giga-Pudding commented Dec 7, 2023

I think it makes more sense, if the script would resolve the DynDNS record on it's own and compare it with the current public IP address, which the script gets from ip.hetzner.com. If the results mismatch, an update is needed.

This way, the Hetzner API would only be used when an DNS record update is necessary and informations are not stored in some file.

@thcrt
Copy link
Contributor

thcrt commented Dec 7, 2023

@Giga-Pudding I feel that caching could make that difficult. The API, on the other hand, will always give the most up-to-date results.

Someone's service being down for up to the length of their TTL every so often would be a horrible problem to try and diagnose. I think anything like that is worth avoiding.

@Giga-Pudding
Copy link

Giga-Pudding commented Dec 8, 2023

hmm, I don't see how caching could impact the update procedure itself, but i agree that caching could be an issue after the record has been successfully updated. When the script is executed the next time, it would maybe update the record again, because the local dns resolver still replies with an old cached record. This would falsely cause the script to think, an update is necessary again. It wouldn't be a problem (the DynDNS record would always be correct), but it would lead to unnecessary updates, as long as the TTL of the old record is not expired.

However, this could easily be avoided by querying Hetzners authoritative Nameservers directly. In that case, there's no caching involved. We can use dig to find them:

all authoritative nameservers:
dig +short NS mydomain.com

only the primary authoritative nameserver (probably better):
dig +short SOA mydomain.com | cut -d' ' -f1

Advantage:

  • no excessive use of Hetzners API

Disadvantage:

  • the script has a new dependency: using the DNS protocol (for example, by using dig)
  • the host that runs the script must be able to reach Hetzners authoritative Nameservers directly. The user of this script should consider this, for firewall reasons.

Also:
i think it would be a good idea to query Hetzners authoritative nameserver anyway, as some sort of verify. If the update was not successfull, another script could be triggered (which would send an e-mail notification or whatever). However, this "verify-step" should ideally happen after waiting 2-3 minutes.

@thcrt
Copy link
Contributor

thcrt commented Dec 11, 2023

@Giga-Pudding You're right, my comment on caching wasn't fully thought-out. Forgive me, I wrote it at midnight ;)

I agree with your proposed approach on a theoretical level, however, I strongly feel that a dependency on the entire BIND suite for dig is an unacceptable compromise. Keep in mind that a common environment for this tool is on a router, and OpenWRT for instance doesn't ship with all of BIND installed. Routers typically have extremely limited flash storage and this additional dependency is therefore a considerable disadvantage. I believe nslookup is present by default on standard builds of OpenWRT, but this might not be the case for all router firmware, and nslookup has its own set of challenges (such as not using the OS's resolver).

You also raise a good point that direct contact to Hetzner nameservers through the DNS protocol may not always be possible. In fact, I'd argue that on many networks, DNS should be firewalled and only available through a local server that caches and queries a known-good resolver, and that this is best practice.

I agree with @shuuryou that hitting the API multiple times per call, even in cases where an update isn't needed, is unnecessary and not ideal. The best approach, in my view, is indeed to store the latest state of the record and only fetch the client's external IP address until it's clear that an update is needed. Something in /tmp would likely be best, as this cache doesn't have to be eternally persistent and we can live with it being wiped on reboot.

I'm considering rewriting this script in a compiled language, likely either C or Rust (both come with their pain points). Should I actually commit to that, I may end up making it a daemon intended to be launched by an init system and run in the background. In that case, the last-set IP address could be stored in memory only.

Either way, the cached IP address should be stored along with the time it was last checked, to account for the possibility that it's changed by something other than the script. It could then be re-checked at a given interval, maybe once every 6 hours or so.

@shuuryou
Copy link
Author

shuuryou commented Dec 11, 2023

For anyone stopping by here who needs a solution today, consider this patch. Barring any bugs (I did this in a few minutes, please excuse any mistakes -- it works for me), it should resolve the issue for most people who care about it without introducing additional dependencies on tools or external REST APIs. It's working only with what's already there.

--- theirs      2023-12-11 16:56:51.219668495 +0100
+++ mine        2023-12-11 17:09:19.196359942 +0100
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 # DynDNS Script for Hetzner DNS API by FarrowStrange
 # v1.3

@@ -117,6 +117,8 @@
   else
     logger Info "Current public IP address: ${cur_pub_addr}"
   fi
+
+  last_ip_file="/tmp/ddns_lastip_${zone_id}_aaaa"
 elif [[ "${record_type}" = "A" ]]; then
   logger Info "Using IPv4, because A was set as record type."
   cur_pub_addr=$(curl -s4 https://ip.hetzner.com | grep -E '^([0-9]+(\.|$)){4}')
@@ -126,11 +128,27 @@
   else
     logger Info "Current public IP address: ${cur_pub_addr}"
   fi
+
+  last_ip_file="/tmp/ddns_lastip_${zone_id}_a"
 else
   logger Error "Only record type \"A\" or \"AAAA\" are support for DynDNS."
   exit 1
 fi

+if [ ! -f "$last_ip_file" ]
+then
+  old_pub_addr="unknown"
+else
+  old_pub_addr=$(<"$last_ip_file")
+fi
+
+if [ "$old_pub_addr" = "$cur_pub_addr" ]; then
+  logger Info "Public ip address has not changed. Nothing to do."
+  exit 0
+fi
+
+echo $cur_pub_addr > $last_ip_file
+
 # get record id if not given as parameter
 if [[ "${record_id}" = "" ]]; then
   record_zone=$(curl -s -w "\n%{http_code}" --location \

@thcrt
Copy link
Contributor

thcrt commented Dec 11, 2023

@shuuryou Note that this patch won't work on pure or nearly-pure POSIX shells like dash, which is a goal of the mainline script. I'm not a script guru and I'm currently rewriting the whole thing in C (I maintain a downstream fork), so I don't really have the time to convert it to pure POSIX, but once the C program is finished I'll probably keep the script around and patch this in.

None of this is criticism, by the way! I really appreciate you putting this up here. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants