Skip to content

CLOUDNS: add registrar support#3961

Merged
tlimoncelli merged 4 commits intoStackExchange:mainfrom
RobinDaugherty:feat/cloudns-registrar
Jan 7, 2026
Merged

CLOUDNS: add registrar support#3961
tlimoncelli merged 4 commits intoStackExchange:mainfrom
RobinDaugherty:feat/cloudns-registrar

Conversation

@RobinDaugherty
Copy link
Copy Markdown
Contributor

@RobinDaugherty RobinDaugherty commented Jan 6, 2026

ClouDNS uses the same API for DNS and registrar-related operations. Following the pattern I saw in other provider/registrar combos (autodns, cscglobal), I implemented the registry call in the cloudnsProvider object type, and when used as a registry it creates an additional instance of the Provider.

Incidentally this causes rate-limiting functionality not to function optimally because each of the instances—the provider and registrar—have their own requestLimit object, causing the rate limit to be tracked and applied within each separately. I'm going to follow up with a PR that will improve the API rate-limit implementation for ClouDNS.

The ClouDNS nameserver API responds with different structures apparently depending on the number of name servers that the domain has. I observed three on the same domain:

  1. When there are 2 domain names for non-ClouDNS name servers, the response is an array of name server strings ["abc.ns.cloudflare.net", "def.ns.cloudflare.net"]
  2. When there are 4 ClouDNS name servers, the response is an object with string keys containing the 4 entries {"1":"pns1.cloudns.net","2":"pns2.cloudns.net","3":"pns3.cloudns.net","4":"pns4.cloudns.net"}
  3. When there are 5 name servers, the response is an object with string keys containing 8 entries included 3 blank values {"1":"pns1.cloudns.net","2":"pns2.cloudns.net","3":"pns3.cloudns.net","4":"pns4.cloudns.net","5":"abc.ns.cloudflare.net","6":"","7":"","8":""}

This covers all of those cases, plus the possible case where the array contains blank strings.

The new setNameservers API call needed to send multiple copies of the same key nameservers[] in the query value, which is supported by the http query values object, but was not possible with the existing call pattern because the get function was built to accept a string map instead of a query values object. So I refactored get into a wrapper function that converts the map into a query values object, which calls the new getWithQuery function with the query values object.

tlimoncelli added a commit that referenced this pull request Jan 6, 2026
There was already a [Limiter](https://pkg.go.dev/golang.org/x/time/rate)
in use here to keep the rate of requests below the apparent limit.

The ClouDNS API doesn't give any sort of proper API response when rate
limit is reached. It's a 200 status code and an error message in the
JSON body, and no headers that would help to track or back off for the
right amount of time.

There was a comment in the implementation that mentions an undocumented
10-per-second limit, while the error message they give today says that
the limit is 20 per second. I kept the settings on the Limiter the same
since 10 per second should be plenty fast.

But it will now retry the request when the rate limit is reached. At the
same time, it "steals" some reservations on `rate.Limiter` to quiet
other concurrent ClouDNS API calls for about half a second. This seems
to be plenty to fix my rate-limit issues. (I tested with 20 domains with
ClouDNS as both registrar and DNS provider using the functionality in
#3961.)

When rate limit is reached, it emits a warn-level message. This follows
a pattern I see in `adguardhome` and `desec` providers but I don't love
it—it's less important and less actionable than other warn-level
messages in the project.

Co-authored-by: Tom Limoncelli <[email protected]>
@tlimoncelli
Copy link
Copy Markdown
Contributor

CC @pragmaton

Wow! This is great!

By the way.... in the future I plan on merging things so that one initializer is used whether the providers is a registrar or a DNS server.

if existingStr != desiredStr {
return []*models.Correction{
{
Msg: fmt.Sprintf("Update nameservers from '%s' to '%s'", existingStr, desiredStr),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use %q instead of %s

@tlimoncelli
Copy link
Copy Markdown
Contributor

I hope to do a release later today. I'm going to merge this. The 1 comment is minor and can be fixed later in a separate PR.

Thanks for adding this!

@tlimoncelli tlimoncelli merged commit 7504767 into StackExchange:main Jan 7, 2026
2 checks passed
tlimoncelli added a commit that referenced this pull request Jan 12, 2026
Fix a format string that used `%s` where `%q` is more appropriate, to
escape the string fully.

This addresses the feedback in #3961 that I didn't get done in time to
be merged.

---------

Co-authored-by: Tom Limoncelli <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants