Request for feedback: I created a desec-centric dyndns client

Hi,

during a recent discussion here, this comment stuck with me. And it led to me writing a desec-centric dyndns client in Python. It tries to implement all thoughts from another comment on that thread.

Here it is: GitHub - neurolabs/desec-dnsupdater , desec-dnsupdater · PyPI

In short, it tries to reduce load spikes on the desec servers, therefore conserving resources, while keeping things simple by taking a desec centric approach.

I would appreciate some feedback, especially of course from @nils and @fiwswe .

Feel free to answer here or to open issues / MRs.

Edit: @KapernMagIchNicht sorry for crossing beams with your efforts in go and bash.

Haha no worries, the more tools we have to ease traffic on deSEC servers, the better.
I might even steal some of your code and ideas like the IPv6 network adapter :grin:

Hi @ole!

First of let me put in the following disclaimer: I don’t use Linux on a regular basis and I’m not an expert on Python code.

That said, here are some quick comments (without any specific order):

  • First, I think using a language such as Python for this is a good idea. Compared to shell scripts some things can be dealt with much better in a “real” programming language IMHO. Of course other languages could be just as good, so my praise is not specifically about using Python.

  • It is also good to have another choice for tools to handle the updates. :+1:

  • The function _get_public_ipv4() apparently uses the URL https://api.ipify.org/ to get the public IPv4 address. This is probably ok, but I’d like to point out that deSEC has its own service for that: https://checkipv4.dedyn.io. I’d advise making this user configurable.

  • The logic of the _get_public_ipv6() function is completely different that that of the _get_public_ipv4() function. Instead of determining the public IPv6 address using an external service it tries to examine the IPv6 addresses of a specified network interface. While not inherently wrong, a comment explaining this different strategy might be helpful.

  • I can’t quite follow the logic of the function _get_public_ipv6(). This might be due to my lack of Python skills. But reading the code it seems to only consider IPs with EUI-64 IIDs as valid public IPv6 addresses? And what about interfaces with multiple valid public IPv6 addresses?

  • The name of the function _mac_to_ipv6_suffix() seems somewhat misleading. From what I understand it tries to construct an EUI-64 IID? If so it should be named accordingly. Or at least a comment should explain this.

  • It appears that the script will use the normal API to update the DNS records, not the IP Update API. That is not ideal because only the IP Update API will set the DNS TTLs to 60 (instead of 3600). For dynDNS (DDNS) you generally want very short TTLs to reduce the potential outage when the public IPs change.

  • There seems to be some randomisation of the update time, which is good, but the parameters used don’t make much sense to me. The goal should be to avoid calling deSEC services at times where other clients are most likely to do the same. This happens to be around full minutes due to many clients using cron(8) to trigger their actions. So the randomisation code should try to avoid a window around full minutes and otherwise distribute the execution randomly in the remaining time.

  • Where is the source for the dependency desec-dns? (This probably shows my lack of Python experience :wink: ) Is it perhaps this one?
    GitHub - s-hamann/desec-dns: A simple deSEC.io API client (i.e. https://pypi.org/project/desec-dns/)

  • Does the script require the deSEC token to passed on the command line? If so then this could be considered a security risk. Other processes could get access to the token that way.

HTH
fiwswe

2 Likes

Thanks for the initial feedback, @KapernMagIchNicht @fiwswe . I’ll respond to some of your thoughts, @fiwswe :

  • making ip detection configurable/pluggable is a good idea I was pondering as well. My IPv6 detection is due to how FritzBoxes handle IPv6 Forwarding, see the README.md for some level of explanation (this post explains it quite well: www.marty44.net - Fritzbox IPv6 Portforwarding ). If you need more information, feel free to ask so I can clarify in the documentation. Providing “normal” methods such as external detection is a very good thing (which does not work for me, because the IPv6 that’s outgoing on my box is one set by privacy extensions). Once IP detection is configurable, I could offer my detection method as one of the options.
  • Using the normal API was a design choice due to less strict rate limits. It seemed to me that this API would lead to less load on the servers. I could change the method to using the Update API. Regarding TTL, using the normal API with records created using the Update API keeps the TTL of 60. So it also could be sensible to do creation using the Update API and updates using the normal API. WDYT?
  • Update time can easily be handled in Python. My current approach uses a one-off 10-20 second delay when called as a one-off (probably cron), which does not spread out the load across the full minute. When called as a “service” (continuous update loop), it waits for a randomly chosen time between the configured wait interval (default 60 seconds) and 10 seconds more (default 70 seconds). It is a very good idea to just look at the clock at the moment of need for an update and then (micro-)schedule the update call within the next minute (although an accurate system clock is a prerequisite). What about a randomly chosen moment in the window :05 to :55 seconds? I would use the builtin sched — Event scheduler — Python 3.13.4 documentation .
  • Yes, you got the right source code. For Python dependencies, one way to find documentation and source code is pypi.org, e.g. Client Challenge will give you a list of python packages with desec in the name. In that list you can find desec-dns.
  • The token (and all other parameters) can also be passed via environment variables, see the README.md

Let me know what you think.

I will be on vacation until July starting this weekend, so don’t interpret silence on my part the wrong way. I will be happy to implement any ideas that come up afterwards.

I only found ways to tell curl which interface to use, but not which IP of said interface. Is that not configurable with curl?

Do I need to use external services for finding my IPv6 when I specify the (global) outgoing IPv6 to use? :wink:

2 Likes

The curl manpage lists --interface as the option to specify an outgoing address:

> -interface <name>
>               Perform an operation using a specified interface.
>               You can enter interface  name,  IP  address  or
>               host name.

But generally speaking, since IPv6 does not usually involve NAT, it should be possible to find the applicable IPv6 address locally, and this is in several ways better than calling out to the internet to learn the address.

Yes, I own a FRITZ!Box myself, so I know how they work.

I don’t think FRITZ!OS requires an EUI-64 IID specifically. It just requires a static IID AFAICT. My experiments with OpenBSD did not reveal any method to manually define a static IID while keeping the automatic update of the IPv6 LAN prefix. So an EUI-64 IID was the only solution I could get to work there. But this is more a limitation of the client OS. I have not researched Linux IPv6 networking to see if the situation might be different there. I don’t use Windows and never had the need to research this on macOS.

Anyway, not everyone uses FRITZ!Box routers, so unless you want to limit your script to their support, a more flexible solution might be a good idea.

Granted, using the local data from the interface configuration saves the need to access an external service such as https://checkipv6.dedyn.io. OTOH figuring out which IPv6 address(es) to use, can be somewhat challenging.

That would be a question for the deSEC folks. I would not have the expectation of this working unless this is documented to work that way.

At the very least it should be documented that you need to use the IP Update API to initially create the records.

Most hosts use NTP to synchronise their clock. So unless that is not the case for your situation I wouldn’t worry about it. In fact the whole issue of high server load at full minutes is caused by all of the clients having synchronised clocks and using cron(8), which tends to start its jobs on full minutes.

Sure. I’d probably increase the window at the start of minute a bit to say :10 - :55, just to give the other clients enough time to do their thing. The 5s at the end are mainly so that our script can finish in time (and to give a bit of tolerance for slight clock offsets).

Also I’d argue that you only need to delay the times the deSEC servers are accessed, not the other stuff the script is doing. So a small function that calculates the next appropriate time and sleeps accordingly, called immediately before any call that accesses the deSEC API would be ideal.

Respecting the Rate Limits is also a very good idea. This could be done by noting the last access time immediately after an API access and taking that into account in the proposed function.

Yes :slight_smile: It could be that your IPv6 is broken for whatever reason.

That is just assuming that your IPv6 works.
Don’t get me wrong, I also do assume that in a different way. I don’t use IPv6 for DynDNS to begin with. Since I only choose ISPs that follow RIPE recommendations (static /48 prefix), I always use preserve instead.
With the downside that I would not get my AAAA record deleted, if for whatever reason I loose IPv6 on my host. So while I do that, I can understand why others would rather like to check online.

Indeed, but I wouldn’t conflate monitoring with dynamic DNS. In order to reliably get the IPv6 address that you want to use from an external host, you practically need to know it before you even ask, due to privacy extensions. You could and should use a more generic method of testing your connectivity if that’s at all necessary.

1 Like
import desec  # type: ignore[import-untyped]

That’s surprising. I thought this module was fully typed. It’s even checked by the CI pipeline. Did you have any issues with the annotations?

This is the error I get from running mypy src:

src/desec_dnsupdater/desec_dyndns.py:13: error: Skipping analyzing "desec": module is installed, but missing library stubs or py.typed marker  [import-untyped]
src/desec_dnsupdater/desec_dyndns.py:13: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports