Skip to content

fix: match ip_type case-insensitively in IONOS and Hetzner providers - #316

Merged
TimothyYe merged 1 commit into
TimothyYe:masterfrom
MaxFreedomPollard:fix/ipv6-record-type-case
Sep 12, 2026
Merged

TimothyYe merged 1 commit into
TimothyYe:masterfrom
MaxFreedomPollard:fix/ipv6-record-type-case

Conversation

@MaxFreedomPollard

@MaxFreedomPollard MaxFreedomPollard commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

The IONOS and Hetzner providers each compare the configured ip_type against one fixed spelling instead of normalising the case first, so both of them go on updating the A record when the user asked for IPv6.

IONOS, internal/provider/ionos/ionos_handler.go:158, compares provider.configuration.IPType against utils.IPV6, which is the string "IPV6". The README documents the value as IPv4 or IPv6, and every sample under configs/ writes IPv4, so a config written from the docs never matches and getRecord queries recordType=A. An IPv6 address never equals the A record's content, so the early return in UpdateIP never fires and the provider PUTs the IPv6 address into the A record on every cycle.

Hetzner, internal/provider/hetzner/hetzner_handler.go:159, compares against the literal "IPv6". That matches the documented spelling but not the one the web UI stores. The toggle in web/components/ip-mode.tsx sends IPV6, and UpdateNetworkSettings at internal/server/controllers/network.go:53 writes that string straight into ip_type and saves the config, so switching to IPv6 mode in the UI leaves Hetzner picking the A record.

Every other provider that reads ip_type already normalises with strings.ToUpper before comparing against utils.IPV6, so these two call sites now do the same. IONOS still accepts AAAA as it did before.

Each provider gets a mock API server test in the style of internal/provider/cloudflare/cloudflare_verify_test.go. The mock zone holds both an A and an AAAA record for the same host, and the test asserts which record UpdateIP looks up and writes for IPv6, IPV6, ipv6, IPv4 and an unset ip_type. To point the providers at the mock they gained an API field set from BaseURL in Init, the same field the Cloudflare provider already carries for this.

Verified on macOS 15 with Go 1.26.4 and golangci-lint v1.64.8, the version the Lint job pins.

$ go test -race -p 4 -count=1 ./...
ok  github.com/TimothyYe/godns/internal/provider/hetzner  1.322s
ok  github.com/TimothyYe/godns/internal/provider/ionos    1.299s
(all other packages ok, none failing)

$ go vet ./internal/provider/...
$ gofmt -l internal/provider/ionos internal/provider/hetzner
$ golangci-lint run --timeout=5m ./...
(no output from any of the three)

Restoring just the two comparisons and keeping the new tests reproduces the bug, which is what the tests are there to catch:

--- FAIL: TestUpdateIPPicksRecordTypeForConfiguredIPType/IPv6
    ionos_handler_test.go:109: ip_type "IPv6": looked up record types [A], want [AAAA]
    ionos_handler_test.go:113: ip_type "IPv6": updated records [recA], want [recAAAA]
--- FAIL: TestUpdateIPSkipsUnchangedRecord
    ionos_handler_test.go:132: expected no record update, got [recA]
--- FAIL: TestUpdateIPPicksRecordTypeForConfiguredIPType/IPV6
    hetzner_handler_test.go:108: ip_type "IPV6": updated record recA of type A, want recAAAA of type AAAA

One thing I left alone: UpdateNetworkSettings compares the incoming ip_mode against utils.IPV4 and utils.IPV6 while GetNetworkSettings hands out c.config.IPType unchanged, so a config holding IPv6 fails both of those checks on the way back in. That is a separate fix in the server and the frontend, happy to send it as its own PR if you want it.

Both providers compared the configured ip_type against a fixed spelling
instead of normalising the case first, so each one picked the A record
when the user asked for IPv6.

ionos_handler.go:158 compared ip_type against utils.IPV6 ("IPV6"). The
README documents the value as "IPv4" or "IPv6" and every sample config
writes "IPv4", so a hand written IPv6 config fell through to the A
record: godns looked up recordType=A and then PUT the IPv6 address into
the A record on every cycle, because an IPv6 address never equals the A
record's content.

hetzner_handler.go:159 compared ip_type against the literal "IPv6". That
matches the documented spelling but not the one the web UI saves. The IP
mode toggle in web/components/ip-mode.tsx sends "IPV6", and
UpdateNetworkSettings in internal/server/controllers/network.go writes
that string straight into ip_type, so switching to IPv6 in the UI left
Hetzner updating the A record.

Every other provider already normalises with strings.ToUpper before
comparing against utils.IPV6, so both call sites now do the same.

Adds a mock API server test for each provider, in the style of
cloudflare_verify_test.go, covering a dual stack host and the "IPv6",
"IPV6" and "ipv6" spellings. To make the base address injectable the two
providers gained an API field set from BaseURL in Init, matching the API
field the Cloudflare provider already uses for the same purpose.
@TimothyYe
TimothyYe merged commit 543cd3a into TimothyYe:master Sep 12, 2026
3 checks passed
@TimothyYe

Copy link
Copy Markdown
Owner

Thanks for your contribution!

TimothyYe added a commit that referenced this pull request Sep 12, 2026
Follow-up to #316. The two providers were merged with their own
strings.ToUpper comparisons because the shared helpers landed on master
at the same time. Route them through utils.RecordType like every other
provider; IONOS keeps accepting the legacy "AAAA" spelling via
strings.EqualFold. The tests added in #316 cover both providers.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
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

Successfully merging this pull request may close these issues.

2 participants