Skip to content

HETZNER_V2: Add provider for Hetzner DNS API#3837

Merged
tlimoncelli merged 12 commits intoStackExchange:mainfrom
das7pad:hetzner-v2
Nov 30, 2025
Merged

HETZNER_V2: Add provider for Hetzner DNS API#3837
tlimoncelli merged 12 commits intoStackExchange:mainfrom
das7pad:hetzner-v2

Conversation

@das7pad
Copy link
Copy Markdown
Collaborator

@das7pad das7pad commented Nov 14, 2025

Closes #3787

This PR is adding a HETZNER_V2 provider for the "new" Hetzner DNS API.

Testing:

  • The integration tests are passing.
  • Manual testing:
    • preview (see diff for existing zone)
    • preview --populate-on-preview (see full diff for newly created zone)
    • push (see full diff; no diff after push)
    • push (see full diff; no diff after push to newly created zone -- i.e. single pass and done)
var REG_NONE = NewRegistrar('none')
var DSP = NewDnsProvider('HETZNER_V2')

D('testing-2025-11-14-7.dev', REG_NONE, DnsProvider(DSP),
    A('@', '127.0.0.1')
)
Details
# push for newly created zone
CONCURRENTLY checking for 1 zone(s)
SERIALLY checking for 0 zone(s)
Waiting for concurrent checking(s) to complete...DONE
******************** Domain: testing-2025-11-14-7.dev
1 correction (HETZNER_V2)
#1: Ensuring zone "testing-2025-11-14-7.dev" exists in "HETZNER_V2"
SUCCESS!
CONCURRENTLY gathering records of 1 zone(s)
SERIALLY gathering records of 0 zone(s)
Waiting for concurrent gathering(s) to complete...DONE
******************** Domain: testing-2025-11-14-7.dev
4 corrections (HETZNER_V2)
#1: ± MODIFY-TTL testing-2025-11-14-7.dev NS helium.ns.hetzner.de. ttl=(3600->300)
± MODIFY-TTL testing-2025-11-14-7.dev NS hydrogen.ns.hetzner.com. ttl=(3600->300)
± MODIFY-TTL testing-2025-11-14-7.dev NS oxygen.ns.hetzner.com. ttl=(3600->300)
SUCCESS!
#2: + CREATE testing-2025-11-14-7.dev A 127.0.0.1 ttl=300
SUCCESS!
Done. 5 corrections.

Feedback for @jooola and @LKaemmerling:

  • The SDK was very useful in getting 80% there! Nice! 🎉
  • Footgun:
    • The result values are not "up-to-date" after waiting for an Action, e.g. Zone.AuthoritativeNameservers.Assigned is not set when Client.Zone.Create() returns and the following "wait" will not update it.
    • Taking a step back here: Waiting for an Action with a separate SDK call does not seem very natural to me. Does the SDK-user need to know that you are processing operations asynchronous? (Which seems like an implementation detail to me, something that the SDK could abstrct over.) Can Client.Zone.Create() return the final Zone instead of the intermediate result?
  • Features missing compared to the DNS Console, in priority order:
    • It is no longer possible to remove your provided name servers from the root/apex. Use-case: dual-home/multi-home zone with fewer than three servers from Hetzner. I'm operating one of these and cannot migrate over until this is fixed.
    • Performance regression due to lack of bulk create/modify. E.g. one of the test suites spends about 4.5 minutes on making creating 100 record-sets and then another 4 minutes for deleting them in sequence again. With your async API, these are create 2*100 + delete 2*100 = 400 API calls. Previously, these were create 1 + delete 100 = 101 API calls. Are you planning on adding batch processing again?
  • Usability nits
    • Compared to other record-set based APIs, upserts for record-sets are missing. This applies to records of a record-set and the ttl of the record-set (see separate SDK calls for the cases diff2.CREATE vs diff2.CHANGE and two calls in diff2.CHANGE for updating the TTL vs records).
    • Some SDK methods return an Action (e.g. Zone.ChangeRRSetTTL()), others wrap the Action in a struct (Client.Zone.CreateRRSet()) -- even when the struct has a single field (ZoneRRSetDeleteResult).

@tlimoncelli
Copy link
Copy Markdown
Contributor

Thanks for sharing this! Looks good so far!

Yes, creating a new provider is often best when SDKs change radically. We had to do with with GANDI_V5 (replaced GANDI), and CLOUDFLARE_API (replaced CLOUDFLARE).

One bit of feedback: Please keep names consistent. Go doesn't like hyphens or underlines in names, so let's avoid that (the Gandi and Cloudflare examples above have trouble with this reguard). Everything should be either hetzner2 or HETZNER2.

For example:

  • providers/hetzner2/hetzner2Provider.go
  • Code should be package hetzner2
  • The internal name should be HETZNER2

@das7pad
Copy link
Copy Markdown
Collaborator Author

das7pad commented Nov 16, 2025

Thanks for the feedback, Tom.

One bit of feedback: Please keep names consistent. Go doesn't like hyphens or underlines in names, so let's avoid that (the Gandi and Cloudflare examples above have trouble with this reguard). Everything should be either hetzner2 or HETZNER2.

I've used the same pattern as the Gandi provider: Go package gandiv5 -> hetznerv2 and user facing label GANDI_V5 -> HETZNER_V2.
Do you want to change the pattern moving forward?
If so, WDYT about aligning all the providers in the next major version of dnscontrol?

  • Go gandiv5 -> gandi5 and label GANDI_V5 -> GANDI5; Alternative: Can we drop the version number now that the other provider has been removed?
  • Go cloudflare -> cloudflareapi; Alternative: Can we drop the api suffix now that the other provider has been removed?
  • Label AZURE_DNS -> AZUREDNS
  • Go doh -> dnsoverhttps

I would be fine with making the change for hetzner2/HETZNER2 today to avoid a breaking change in the next major version for the new provider.

@jooola
Copy link
Copy Markdown
Contributor

jooola commented Nov 17, 2025

@das7pad Thanks for the feedback, I really appreciate it.

The result values are not "up-to-date" after waiting for an Action, e.g. Zone.AuthoritativeNameservers.Assigned is not set when Client.Zone.Create() returns and the following "wait" will not update it.

This is expected, you should fetch the zone again after the action completed, there is no way around this.

My take away is that more documentation would have been helpful to better understand how to use the SDK/API with regards to actions.

Taking a step back here: Waiting for an Action with a separate SDK call does not seem very natural to me. Does the SDK-user need to know that you are processing operations asynchronous? (Which seems like an implementation detail to me, something that the SDK could abstrct over.) Can Client.Zone.Create() return the final Zone instead of the intermediate result?

We do no want to assume how a client want to behave, waiting for actions ourselves would assume too much of the user application's behavior.

We could indeed build a layer on top, to provide the user with such convenience features, I'll take the feedback with me and see what we can do.

Performance regression due to lack of bulk create/modify. E.g. one of the test suites spends about 4.5 minutes on making creating 100 record-sets and then another 4 minutes for deleting them in sequence again. With your async API, these are create 2100 + delete 2100 = 400 API calls. Previously, these were create 1 + delete 100 = 101 API calls. Are you planning on adding batch processing again?

You can decouple calling the API and waiting for the returned actions. This should cut the waiting time by a lot. I'll add more comment about this in the PR review it self.

Let me know if after this improvement, you still think a batch processing API is needed?

Some SDK methods return an Action (e.g. Zone.ChangeRRSetTTL()), others wrap the Action in a struct (Client.Zone.CreateRRSet()) -- even when the struct has a single field (ZoneRRSetDeleteResult).

Yes, this is a pattern that we follow to prevent future breaking changes if we ever want to add a field to the Create/Delete operations.

This is expected.


I'll address more of the feedback in the pull request review.

Copy link
Copy Markdown
Contributor

@jooola jooola left a comment

Choose a reason for hiding this comment

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

It is no longer possible to remove your provided name servers from the root/apex. Use-case: dual-home/multi-home zone with fewer than three servers from Hetzner. I'm operating one of these and cannot migrate over until this is fixed.

But this could still work if you keep all three existing NS, and add your additional NS on top of that? Is there a problem with requiring to use all 3 existing Hetzner name servers?

@tlimoncelli
Copy link
Copy Markdown
Contributor

I'm going to discuss the naming issue at the monthly video call today: #3798

  • What names to use for the new HETZNER V2 provider?
    • Right now we have inconsistent naming. Do we follow the current pattern, start a new one?
      • Go styleguide discourages underlines in module names.
      • We have inconsistent names:
        • AZURE_DNS -- providers/azuredns/azureDnsProvider.go -- package azuredns
        • CLOUDFLAREAPI -- providers/cloudflare/cloudflareProvider.go -- package cloudflare
        • GANDI_V5 -- providers/gandiv5/gandi_v5Provider.go -- package gandiv5
      • Renaming providers is hard because it breaks existing configs. I’ve been considering adding “provider aliases” to make renames easier. For example, we could make CLOUDFLARE an alias for CLOUDFLAREAPI and output warnings to encourage switching to the new name.
    • Options:
      • Maintain the existing inconsistency:
        • HETZNER_V2 -- providers/hetznerv2/hetznerv2Provider.go -- package hetznerv2
      • Start a new standard: (and rename the others some day)
        • HETZNERV2 -- providers/hetznerv2/hetznerv2Provider.go -- package hetznerv2
      • Remove the "V"?
        • HETZNER2 -- providers/hetzner2/hetzner2Provider.go -- package hetzner2
      • Something else?
        • ???

@tlimoncelli
Copy link
Copy Markdown
Contributor

I'm going to discuss the naming issue at the monthly video call today: #3798

We discussed this. We have a lot of inconsistent naming and the discussion was about whether we should try to fix the old names and what should we do with future names.

Our conclusion was:

  1. Allow slugs to include underlines for readability (AZURE_DNS, HETZNER_v2), but everything else should be "downcase and remove any underlines").
  • echo AZURE_DNS | tr A-Z a-z | tr -d _ outputs "azuredns"
  • echo HETZNER_V2 | tr A-Z a-z | tr -d _ outputs "hetznerv2"
  1. The recommendation for Hetzner:
  • Slug: HETZNER_V2
  • Directory/filename: providers/hetznerv2/hetznerv2Provider.go
  • Package name: package hetznerv2
  1. In the future, we'll make it easier to rename old providers by adding some kind of aliasing mechanism so that the old and new names work.

@das7pad
Copy link
Copy Markdown
Collaborator Author

das7pad commented Nov 18, 2025

3. The recommendation for Hetzner:

  • Slug: HETZNER_V2
  • Directory/filename: providers/hetznerv2/hetznerv2Provider.go
  • Package name: package hetznerv2

👍 Done in 7bcc943.

The integration tests are passing after merging main.

Copy link
Copy Markdown
Contributor

@tlimoncelli tlimoncelli left a comment

Choose a reason for hiding this comment

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

The code looks excellent! Only one minor comment.

Some other things:

  • .github/workflows/pr_integration_tests.yml: Please add HETZNERV2 to the PROVIDERS list.
  • .goreleaser.yml: Search for Provider-specific changes and add hetznerv2

Thanks!
Tom

@das7pad
Copy link
Copy Markdown
Collaborator Author

das7pad commented Nov 19, 2025

The code looks excellent! Only one minor comment.

Awesome, thanks for the feedback!

Some other things:

  • .github/workflows/pr_integration_tests.yml: Please add HETZNERV2 to the PROVIDERS list.
  • .goreleaser.yml: Search for Provider-specific changes and add hetznerv2

Done in daaba36.

@jooola Can you provide us/Tom with test credentials for running in CI?

@das7pad
Copy link
Copy Markdown
Collaborator Author

das7pad commented Nov 19, 2025

It is no longer possible to remove your provided name servers from the root/apex. Use-case: dual-home/multi-home zone with fewer than three servers from Hetzner. I'm operating one of these and cannot migrate over until this is fixed.

But this could still work if you keep all three existing NS, and add your additional NS on top of that? Is there a problem with requiring to use all 3 existing Hetzner name servers?

My registrar used to limit the number of custom nameservers to 5. Trying this again today, it looks like that limitation no longer exists. 7 NS entries is a little excessive, but it will be fine. 😅

@das7pad
Copy link
Copy Markdown
Collaborator Author

das7pad commented Nov 19, 2025

All the points should be addressed now. @jooola Do you want to take another look before we merge this?

@LKaemmerling
Copy link
Copy Markdown
Contributor

@das7pad, regarding test credentials: You should be able to simply register yourself and generate the credentials on your own :) As DNS is free, you won't be charged anything.

Copy link
Copy Markdown
Contributor

@LKaemmerling LKaemmerling left a comment

Choose a reason for hiding this comment

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

Looks good from my perspective, i did not check the code in depth but the documentation and found a few missing ✅

@jooola
Copy link
Copy Markdown
Contributor

jooola commented Nov 20, 2025

Aside from the idna encode on the record names question, this looks good to me 🚀

Co-authored-by: "Lukas Kämmerling"
 <[email protected]>
@das7pad
Copy link
Copy Markdown
Collaborator Author

das7pad commented Nov 21, 2025

(Force pushed to fix attribution with Umlaut)


@das7pad, regarding test credentials: You should be able to simply register yourself and generate the credentials on your own :) As DNS is free, you won't be charged anything.

As far as I can tell, API tokens cannot be scoped to a (free) product, here DNS. Hence I would prefer for you to provide a token.

@das7pad
Copy link
Copy Markdown
Collaborator Author

das7pad commented Nov 30, 2025

The integration tests are passing with testing-2025-11-30-ä.dev as domain.

We are good to merge this. The credentials for CI can be provided later, preferably by Hetzner.

@tlimoncelli
Copy link
Copy Markdown
Contributor

Thanks for contributing this!

Let's discuss the credentials via email. We have a secure way to send secrets. Contact me at tlimoncelli at stack over flow dot com for details.

Tom

@tlimoncelli tlimoncelli merged commit 1e67585 into StackExchange:main Nov 30, 2025
6 checks passed
@GaetanLepage
Copy link
Copy Markdown

Thanks for this feature! Any idea when the next release of dnscontrol will be out?

@tlimoncelli
Copy link
Copy Markdown
Contributor

Hopefully this week. I want to merge #3879 first, then it should ship within 24 hours.

GaetanLepage added a commit to nixos-cuda/infra that referenced this pull request Dec 10, 2025
This gives us the latest release of dnscontrol which supports the
Hetzner v2 provider.

StackExchange/dnscontrol#3837
NixOS/nixpkgs#469113
@nomeata
Copy link
Copy Markdown

nomeata commented Feb 12, 2026

For those watching this issue and wondering if their distro ships this feature yet: It was released as part of 4.28.1: https://github.com/StackExchange/dnscontrol/releases/tag/v4.28.1

Thanks for your work here, @tlimoncelli !

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.

Migrate to the new Hetzner DNS API

7 participants