From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mail02.haj.ipfire.org (localhost [IPv6:::1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4cLHpS6CCNz30N5 for ; Mon, 08 Sep 2025 19:59:12 +0000 (UTC) Received: from mail01.ipfire.org (mail01.haj.ipfire.org [IPv6:2001:678:b28::25]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mail01.haj.ipfire.org", Issuer "R13" (verified OK)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4cLHpS1RRDz2yBF for ; Mon, 08 Sep 2025 19:59:12 +0000 (UTC) Received: from hosted.mailcow.de (hosted.mailcow.de [5.1.76.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mail01.ipfire.org (Postfix) with ESMTPS id 4cLHpP5Dnmzm7 for ; Mon, 08 Sep 2025 19:59:09 +0000 (UTC) Authentication-Results: mail01.ipfire.org; dkim=pass header.d=liftingcoder.com header.s=default header.b=ItQy1L42; spf=none (mail01.ipfire.org: domain of bernhard@liftingcoder.com has no SPF policy when checking 5.1.76.202) smtp.mailfrom=bernhard@liftingcoder.com; dmarc=none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=lists.ipfire.org; s=202003rsa; t=1757361549; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:dkim-signature; bh=kpNiBy3VunOQM/VgPrCPKZWfcP3ryYbJsOAPkzKD+VE=; b=D9abp+yBnpUW9ht0q+QZd2lZJ6pPS1vZtwPhuGZk82Kxg2akKlgCP8L5IIKwfv5wJ3zYRu 9KCKk0G8EC9T8bKCgXmNZQIPpZ19ggeuU6CLBoavDkkHJfc2rMJf+OKkor6db0IKJEUBAy fOfQsJ8hK008hjUdZ1upFw5MUUZgrrDRje8BKKojq6iKvHFf7aE65JjAYG5joNiNjplnTL 0Y+jBbReCMpLIadkZLY4vl64aMbYRt8CGByV6djGT5+cgrq6VJ2KlLGDyx2F+DxShI8YYl S9al8jIxj/M/rBTmrROqKN4s+BVxmLsCbHiSIraz0Je03VEwsMgfYistTqiWKg== ARC-Seal: i=1; s=202003rsa; d=lists.ipfire.org; t=1757361549; a=rsa-sha256; cv=none; b=Y98gTKpr4jv1QsxVRxOlyzj/MYwfX4vzwnFx87SAU81tvvRTzTgSq1B71L7BUSoM05knj4 rAMcGS2mdbbz72qNqOQvOD4qMToj214KnoFoToRXD3HKGm40Yv3XOGxBqKk4LchfVFQ/TC 35JQXdAmZr3CU5Ir0ceWKjpPjmKpVzH1Nb/m5P7xhiXvRHVz+gcft82dbsaJodCX0qACrE JVHw30tiSp+MBbCyLi/IbSIJ0swz/Z0bnP1Q5S5Gen093IeqoZ53nAXCQytkG8uNzogtRL hzGeJG1L5KA9LFZSbBCZRnL24srXESkSvJaFan7JAculM8hyfh8wKutq2JA86Q== ARC-Authentication-Results: i=1; mail01.ipfire.org; dkim=pass header.d=liftingcoder.com header.s=default header.b=ItQy1L42; spf=none (mail01.ipfire.org: domain of bernhard@liftingcoder.com has no SPF policy when checking 5.1.76.202) smtp.mailfrom=bernhard@liftingcoder.com; dmarc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=liftingcoder.com; s=default; t=1757361547; h=from:subject:date:message-id:to:mime-version:content-type: content-transfer-encoding:in-reply-to:references; bh=kpNiBy3VunOQM/VgPrCPKZWfcP3ryYbJsOAPkzKD+VE=; b=ItQy1L42H8HDPRX7YNDQPRjDX14ITr118cP5gbwnfqiSuNtp23+icc5pKDWTvj5IGBwJ+S Ai1Xet5TdX0x7TphYq3s3qjr7ew0xRNma14zYm/5Y3vr83uIOgThqgJkPa/5ma6NvekNSm 4QQmVrLnRdqr5PKMtp4jbpj/xhXvT78= Received: from [192.168.2.10] (80-109-193-217.cable.dynamic.surfer.at [80.109.193.217]) (Authenticated sender: bernhard@beroun.io) by hosted.mailcow.de (Postcow) with ESMTPSA id 7A15B5C0653 for ; Mon, 8 Sep 2025 21:59:07 +0200 (CEST) Message-ID: <6ad2af3a-fd4d-4615-9dbb-15a6fd9589ee@liftingcoder.com> Date: Mon, 8 Sep 2025 21:59:07 +0200 Precedence: list List-Id: List-Subscribe: , List-Unsubscribe: , List-Post: List-Help: Sender: Mail-Followup-To: MIME-Version: 1.0 Subject: Re: [PATCH] ddns: add Cloudflare (v4) provider using API token To: ddns@lists.ipfire.org References: <2139f7-68b17f00-3-48fa3400@237050521> <32702E44-5264-4D75-8645-F297E4AF920B@ipfire.org> <2fd1aa-68b1ec80-2b-633f1b80@145901099> <13f39e-68b60000-1-59b1d300@94062361> Content-Language: en-US From: "Bernhard B." In-Reply-To: Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit X-Spamd-Result: default: False [-6.20 / 11.00]; BAYES_HAM(-2.81)[99.19%]; R_DKIM_ALLOW(-1.44)[liftingcoder.com:s=default]; NEURAL_HAM(-1.00)[-1.000]; DKIM_REPUTATION(-0.73)[-0.72806509974881]; MIME_GOOD(-0.10)[text/plain]; RCVD_IN_DNSWL_LOW(-0.10)[5.1.76.202:from]; MX_GOOD(-0.01)[]; IP_REPUTATION_HAM(-0.00)[asn: 34549(0.00), country: DE(-0.00), ip: 5.1.76.202(0.00)]; TO_MATCH_ENVRCPT_ALL(0.00)[]; RCVD_TLS_ALL(0.00)[]; R_SPF_NA(0.00)[no SPF record]; FUZZY_RATELIMITED(0.00)[rspamd.com]; DMARC_NA(0.00)[liftingcoder.com]; ARC_NA(0.00)[]; RCPT_COUNT_ONE(0.00)[1]; DKIM_TRACE(0.00)[liftingcoder.com:+]; MISSING_XM_UA(0.00)[]; RCVD_COUNT_ONE(0.00)[1]; TO_DN_NONE(0.00)[]; FROM_EQ_ENVFROM(0.00)[]; FROM_HAS_DN(0.00)[]; MIME_TRACE(0.00)[0:+]; PREVIOUSLY_DELIVERED(0.00)[ddns@lists.ipfire.org]; MID_RHS_MATCH_FROM(0.00)[]; RECEIVED_SPAMHAUS_PBL(0.00)[80.109.193.217:received]; ASN(0.00)[asn:34549, ipnet:5.1.76.0/24, country:DE]; RCVD_VIA_SMTP_AUTH(0.00)[]; ARC_SIGNED(0.00)[lists.ipfire.org:s=202003rsa:i=1] X-Rspamd-Server: mail01.haj.ipfire.org X-Rspamd-Action: no action X-Rspamd-Queue-Id: 4cLHpP5Dnmzm7 Hello Michael, I just had a quick look at the API documentation you sent me and as far as I understood the endpoint, it still requires the dns record id when one wants to "PUT" the changes. I've also found this blog post from Cloudflare (https://blog.cloudflare.com/batched-dns-changes/) where they pretty much mention the same: "Each list of records []Record will follow the same requirements as the regular API, except that the record ID on deletes, patches, and puts will be required within the Record object itself." To me it looks like there is no no other way than doing multiple HTTP requests - at least I can't think of any alternative. I guess in theory the zone id and/or the dns record id could be cached, but I think that could also lead to some problems. e.g: As far as I've seen the zone id is static and can't be changed, but I does seem to change if one re-links (i.e delete + add) the domain to Cloudflare. So, if someone does that and the zone id is already cached, the DDNS update wouldn't work anymore. Please let me know in case I've missed something. Cheers Bernhard On 9/2/25 16:15, Michael Tremer wrote: > Hello Bernhard, > >> On 1 Sep 2025, at 21:19, Bernhard Beroun wrote: >> >> Hello Michael, >> >> thanks for adding the changes to the core - I'll adapt the changeset to make use of it. > I pushed these into a separate branch, so please feel free to test them well. I did not really have a good way to test the code much because nothing is really using it, yet. > >> What I do not fully understand though is, what the actual benefit of using the PUT (https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/update/) endpoint compared to the PATCH (https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/) endpoint, which currently using, is. >> >> As far as I've seen, both the PUT and the PATCH endpoint require a zone_id and a dns_record_id, which both need to be fetched upfront. To me both endpoints seems to be pretty much equal in terms of necessary roundtrips? Or am I missing something (obvious) here? > Yes, I agree that this is not entirely obvious what the difference is. > > I understand from the PATCH method, that the records have to exist before and they can be updated. Hence there is some extra code required to create the records if they did not exist before. That just requires more roundtrips, more code, and therefore more things that could go wrong. > > I am understanding the PUT method has something very similar, but it does not have any prerequisites. The records don’t have to exist and if they do, that does not matter to us (except CNAME which is mentioned in the documentation). However, you are absolutely correct, that it would require some kind of record ID. That makes very little sense for us. > > But… maybe I should have explained myself better in my previous email… I think this might all come together in this API call: > > https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/batch/ > > We can batch multiple things together and there is the option to “PUT” some new records in there. Exactly what we would need. We just send the new records, don’t have to consider anything beforehand, and simply let the API update the zone. > > That way, we would have only one call (outside fetching that stupid zone ID) and therefore we only have to do error handling once. Either the update was successful or it was not. That seems to be a much nicer and cleaner approach. > > Since you have access to the API, can you confirm that my view of this is current with a couple of cURL calls maybe? > > -Michael > >> Thanks a lot! >> >> Cheers >> Bernhard >> >> Am Sonntag, August 31, 2025 16:38 CEST, schrieb Michael Tremer : >> >> >>> Hello, >>> >>> Well, some of the providers are actually not (too) well tested. That is because I simply don’t have an account with all of them. So I don’t feel bad to add IPv6 here without having it tested, because if it does not work, we won’t loose anything. >>> >>> I have studied your patch a little and I have taken some changes from it that should go into the core: >>> >>> https://git.ipfire.org/?p=ddns.git;a=shortlog;h=refs/heads/json >>> >>> This is basically that the request function can process JSON better without having to care about all the encoding/decoding stuff in the provider handler. I think that will make the code much more readable. >>> >>> Then, it concerns me that we will have to send so many requests. That does not sound very efficient to me. And maybe we don’t have to do this, because there is another option: >>> >>> https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/update/ >>> >>> That way, we can just send the update as a single PUT operation and replace whatever has been there before. That should work. We are even able to send an A and AAAA record in one go. The only annoying part that remains is fetching the ID of the zone. I am not sure whether ddns should have the ability to cache data like this. >>> >>> -Michael >>> >>>> On 29 Aug 2025, at 19:08, Bernhard Beroun wrote: >>>> >>>> Hello Michael, >>>> >>>> as far as I know Cloudflare also supports IPv6 for DNS, but since I do not use any IPv6 in my home network, I haven't looked into that any further. I also didn't want to add anything I can't thoroughly test myself, therefore I decided to only add IPv4. I was hoping that it's possible to get the IPv4 part into the codebase and let others build upon that and add the missing IPv6 support. But I also totally understand if that's something you don't want to do. >>>> >>>> Cheers, >>>> Bernhard >>>> >>>> >>>> Am Freitag, August 29, 2025 12:25 CEST, schrieb Michael Tremer : >>>> >>>> >>>>> Hello Bernhard, >>>>> >>>>> No you are not hijacking. >>>>> >>>>> I don’t know why your patch did not receive any feedback. It must have fallen off the radar. I am not even the maintainer for DDNS… >>>>> >>>>> Your patch looks much more like it because it is using and extending the code of DDNS. >>>>> >>>>> However, it also only implements IPv4. Does Cloudflare not support IPv6 for DNS at all? >>>>> >>>>> -Michael >>>>> >>>>>> On 29 Aug 2025, at 11:20, Bernhard Beroun wrote: >>>>>> >>>>>> Hi everybody, >>>>>> I don't want to hijack this mail thread here, but just wanted to let you know that I've also submitted a patch a few months ago. See https://lists.ipfire.org/ddns/19993045-2374-4537-96a2-5e2b1900a750@liftingcoder.com/T/#u >>>>>> Unfortunately I never received any feedback, so I assumed Cloudflare support wasn't desired. Again, sorry for hijacking this email chain here, just wanted to quickly drop it here, since it matches the topic. >>>>>> Have a nice day, >>>>>> Bernhard >>>>>> >>>>>> Am Freitag, August 29, 2025 11:50 CEST, schrieb Michael Tremer : >>>>>> >>>>>> >>>>>>> Hello Chris, >>>>>>> >>>>>>> You don’t need to submit any patches more than once. We will get back to you as soon as there is time. >>>>>>> >>>>>>> So let’s get into this... >>>>>>> >>>>>>>> On 29 Aug 2025, at 09:16, Chris Anton wrote: >>>>>>>> >>>>>>>> From: faithinchaos21 <45313722+faithinchaos21@users.noreply.github.com> >>>>>>> I assume that you are faithinchaos21 and this code did not come from someone else? >>>>>>> >>>>>>>> Date: Wed, 27 Aug 2025 01:22:46 -0500 >>>>>>>> Subject: [PATCH] ddns: add Cloudflare (v4) provider using API token >>>>>>>> MIME-Version: 1.0 >>>>>>>> Content-Type: text/plain; charset=UTF-8 >>>>>>>> Content-Transfer-Encoding: 8bit >>>>>>>> >>>>>>>> This adds a provider “cloudflare.com-v4” that updates an A record >>>>>>>> via Cloudflare’s v4 API using a Bearer token. The token is accepted >>>>>>>> from either ‘token’ or legacy ‘password’ for UI compatibility. >>>>>>>> >>>>>>>> Tested on IPFire 2.29 / Core 196: >>>>>>>> - no-op if A already matches WAN IP >>>>>>>> - successful update when WAN IP changes >>>>>>>> - logs include CFv4 breadcrumbs for troubleshooting >>>>>>> To make it short, this patch is sadly very far away from what is acceptable. >>>>>>> >>>>>>> Before we get into the actual implementation, there are many design issues here that simply cannot be accepted into DDNS: >>>>>>> >>>>>>> * This patch only adds support for IPv4. As far as I am aware, Cloudflare is capable of IPv6, too. Why would this be limited to IPv4 only? >>>>>>> >>>>>>> * You are not using the scaffolding and tools that DDNS is providing. A lot of code is being written with plain Python and throwing away all the features that DDNS provides (catching Exceptions, proxy support and many, many more…) >>>>>>> >>>>>>> * This all looks very AI-generated to me and is implemented on a green field. You are even importing all the modules that you need and ignore everything else from DDNS. Why not use this snippet as a standalone script then? There was no consideration for what code existed already. >>>>>>> >>>>>>>> Signed-off-by: Chris Anton >>>>>>>> --- >>>>>>>> 563f089d0820bd61ad4aecac248d5cc1f2adfc81 >>>>>>>> src/ddns/providers.py | 121 ++++++++++++++++++++++++++++++++++++++++++ >>>>>>>> 1 file changed, 121 insertions(+) >>>>>>>> >>>>>>>> diff --git a/src/ddns/providers.py b/src/ddns/providers.py >>>>>>>> index 59f9665..df0f3a9 100644 >>>>>>>> --- a/src/ddns/providers.py >>>>>>>> +++ b/src/ddns/providers.py >>>>>>>> @@ -341,6 +341,127 @@ def have_address(self, proto): >>>>>>>> >>>>>>>> return False >>>>>>>> >>>>>>>> +class DDNSProviderCloudflareV4(DDNSProvider): >>>>>>>> + """ >>>>>>>> + Cloudflare v4 API using a Bearer Token. >>>>>>>> + Put the API Token in the 'token' OR 'password' field of the DDNS entry. >>>>>>>> + Optional in ddns.conf: >>>>>>>> + proxied = false|true (default false; keep false for WireGuard) >>>>>>>> + ttl = 1|60|120... (default 1 = 'automatic') >>>>>>>> + """ >>>>>>>> + handle = "cloudflare.com-v4" >>>>>>>> + name = "Cloudflare (v4)" >>>>>>>> + website = "https://www.cloudflare.com/" >>>>>>>> + protocols = ("ipv4",) >>>>>>>> + supports_token_auth = True >>>>>>>> + holdoff_failure_days = 0 >>>>>>>> + >>>>>>>> + def _bool(self, key, default=False): >>>>>>>> + v = str(self.get(key, default)).strip().lower() >>>>>>>> + return v in ("1", "true", "yes", "on") >>>>>>>> + >>>>>>> This is a good example of a function which totally goes against the coding style of the rest of the code base. It uses str(), chains a lot of functions to gather to make the code look shorter, but very difficult to read. >>>>>>> >>>>>>>> + def update(self): >>>>>>>> + import json, urllib.request, urllib.error >>>>>>> Just no. We don’t import anything further down the line. DDNS provides a toolkit of what to use and you should stay within it. If some functionally is missing, DDNS’ functionality should be extended so that other providers can re-use the same well-maintained and tested code base. >>>>>>> >>>>>>>> + >>>>>>>> + tok = self.get("token") or self.get("password") >>>>>>>> + if not tok: >>>>>>>> + raise DDNSConfigurationError("API Token (password/token) >>>>>>>> is missing.") >>>>>>>> + >>>>>>>> + proxied = self._bool("proxied", False) >>>>>>>> + try: >>>>>>>> + ttl = int(self.get("ttl", 1)) >>>>>>>> + except Exception: >>>>>>>> + ttl = 1 >>>>>>> A TTL of one second is never a good idea. >>>>>>> >>>>>>>> + >>>>>>>> + headers = { >>>>>>>> + "Authorization": "Bearer {0}".format(tok), >>>>>>>> + "Content-Type": "application/json", >>>>>>>> + "User-Agent": "IPFireDDNSUpdater/CFv4", >>>>>>>> + } >>>>>>> Since you are not using the internal request handler, you are creating your own headers and a completely nonsensical user agent. Why? >>>>>>> >>>>>>>> + >>>>>>>> + # --- find zone --- >>>>>>>> + parts = self.hostname.split(".") >>>>>>>> + if len(parts) < 2: >>>>>>>> + raise DDNSRequestError("Hostname '{0}' is not a valid >>>>>>>> domain.".format(self.hostname)) >>>>>>>> + >>>>>>>> + zone_id = None >>>>>>>> + zone_name = None >>>>>>>> + for i in range(len(parts) - 1): >>>>>>>> + candidate = ".".join(parts[i:]) >>>>>>>> + url = >>>>>>>> f"https://api.cloudflare.com/client/v4/zones?name={candidate}" >>>>>>>> + try: >>>>>>>> + req = urllib.request.Request(url, headers=headers, >>>>>>>> method="GET") >>>>>>>> + with urllib.request.urlopen(req, timeout=20) as r: >>>>>>>> + data = json.loads(r.read().decode()) >>>>>>>> + except Exception as e: >>>>>>>> + raise DDNSUpdateError(f"Failed to query Cloudflare >>>>>>>> zones API: {e}") >>>>>>>> + >>>>>>>> + if data.get("success") and data.get("result"): >>>>>>>> + zone_id = data["result"][0]["id"] >>>>>>>> + zone_name = candidate >>>>>>>> + break >>>>>>> It is not acceptable to build anything custom that sends a request. You are removing all other kinds of features that I have mentioned above. >>>>>>> >>>>>>> To just “guess” the name of the zone is something that I would not consider good style. >>>>>>> >>>>>>>> + >>>>>>>> + if not zone_id: >>>>>>>> + raise DDNSRequestError(f"Could not find a Cloudflare Zone >>>>>>>> for '{self.hostname}'.") >>>>>>>> + >>>>>>>> + logger.info("CFv4: zone=%s id=%s", zone_name, zone_id) >>>>>>>> + >>>>>>>> + # --- get record --- >>>>>>>> + rec_url = >>>>>>>> f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records?type=A&name={self.hostname}" >>>>>>>> + try: >>>>>>>> + req = urllib.request.Request(rec_url, headers=headers, >>>>>>>> method="GET") >>>>>>>> + with urllib.request.urlopen(req, timeout=20) as r: >>>>>>>> + rec_data = json.loads(r.read().decode()) >>>>>>>> + except Exception as e: >>>>>>>> + raise DDNSUpdateError(f"Failed to query Cloudflare DNS >>>>>>>> records API: {e}") >>>>>>>> + >>>>>>>> + if not rec_data.get("success"): >>>>>>>> + errs = rec_data.get("errors") or [] >>>>>>>> + if any("Authentication error" in (e.get("message", "") or >>>>>>>> "") for e in errs): >>>>>>>> + raise DDNSAuthenticationError("Invalid API Token.") >>>>>>>> + raise DDNSUpdateError(f"Cloudflare API error finding >>>>>>>> record: {errs}") >>>>>>> Same as above, hardcoded defaults like the timeout. Spaghetti code. >>>>>>> >>>>>>>> + >>>>>>>> + results = rec_data.get("result") or [] >>>>>>>> + if not results: >>>>>>>> + raise DDNSRequestError(f"No A record found for >>>>>>>> '{self.hostname}' in zone '{zone_name}'.") >>>>>>>> + >>>>>>>> + record_id = results[0]["id"] >>>>>>>> + stored_ip = results[0]["content"] >>>>>>>> + logger.info("CFv4: record_id=%s stored_ip=%s", record_id, stored_ip) >>>>>>>> + >>>>>>>> + # --- compare IPs --- >>>>>>>> + current_ip = self.get_address("ipv4") >>>>>>>> + logger.info("CFv4: current_ip=%s vs stored_ip=%s", >>>>>>>> current_ip, stored_ip) >>>>>>>> + if current_ip == stored_ip: >>>>>>>> + logger.info("CFv4: no update needed") >>>>>>>> + return >>>>>>> Why is this done using the API at all? We have functionality to use DNS for this which creates a lot less load than performing several API requests every couple of minutes. >>>>>>> >>>>>>>> + >>>>>>>> + # --- update --- >>>>>>>> + upd_url = >>>>>>>> f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{record_id}" >>>>>>>> + payload = { >>>>>>>> + "type": "A", >>>>>>>> + "name": self.hostname, >>>>>>>> + "content": current_ip, >>>>>>>> + "ttl": ttl, >>>>>>>> + "proxied": proxied, >>>>>>>> + } >>>>>>>> + logger.info("CFv4: updating %s -> %s (proxied=%s ttl=%s)", >>>>>>>> self.hostname, current_ip, proxied, ttl) >>>>>>>> + >>>>>>>> + try: >>>>>>>> + req = urllib.request.Request( >>>>>>>> + upd_url, data=json.dumps(payload).encode(), >>>>>>>> headers=headers, method="PUT" >>>>>>>> + ) >>>>>>>> + with urllib.request.urlopen(req, timeout=20) as r: >>>>>>>> + upd = json.loads(r.read().decode()) >>>>>>>> + except Exception as e: >>>>>>>> + raise DDNSUpdateError(f"Failed to send update request to >>>>>>>> Cloudflare: {e}”) >>>>>>>> >>>>>>> Once again the same custom request logic. >>>>>>> >>>>>>>> + if not upd.get("success"): >>>>>>>> + raise DDNSUpdateError(f"Cloudflare API error on update: >>>>>>>> {upd.get('errors')}") >>>>>>>> + >>>>>>>> + logger.info("CFv4: update ok for %s -> %s", self.hostname, current_ip) >>>>>>>> + return >>>>>>>> + >>>>>>>> >>>>>>>> class DDNSProtocolDynDNS2(object): >>>>>>>> """ >>>>>>>> >>>>>>> I would really like to have support for Cloudflare in DDNS, but this is sadly not the way. >>>>>>> >>>>>>> Please study my comments thoroughly and then lets come up with a plan together how to implement this properly. >>>>>>> >>>>>>> Best, >>>>>>> -Michael >>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>>> >>>>>> >>>>> >>>>> >>>> >>>> >>>> >>> >> >> >> >