Skip to content

feat(infoblox): add AAAA (IPv6) record support - #69

Open
MaxRink wants to merge 2 commits into
AbsaOSS:mainfrom
MaxRink:feat/aaaa-record-support
Open

MaxRink wants to merge 2 commits into
AbsaOSS:mainfrom
MaxRink:feat/aaaa-record-support

Conversation

@MaxRink

@MaxRink MaxRink commented Aug 6, 2026

Copy link
Copy Markdown

Closes #37.

What

Adds AAAA (IPv6) records end-to-end, following the existing patterns for A records.

  • ToAAAAResponseMap() maps []ibclient.RecordAAAA onto the shared ResponseMap using the ibclient.AaaaRecord type constant.
  • Records() fetches record:aaaa per zone through PagingGetObject, the same way A records are fetched (same View/Ea/Zone handling, isNotFoundError tolerance and FailedApiCallsTotal accounting).
  • ToHostAAAAResponseMap() surfaces the Ipv6Addrs of host records as AAAA endpoints, mirroring how Ipv4Addrs already become A endpoints. Both host mappers now skip records carrying no address of the respective family, so an IPv4-only or IPv6-only host record no longer yields an endpoint with an empty target.
  • recordSet() builds a RecordAAAA for create/update/delete, looking the existing object up by name + ipv6addr.
  • getRefID() resolves the object reference and log fields for RecordAAAA.

Infoblox can return the same IPv6 address more than once for a name (the same address in a different but equivalent notation). Left alone that produces a permanently dirty plan, so ToAAAAResponseMap de-duplicates targets via a new ResponseDetails.Contains helper.

Why

The provider handled A, CNAME, TXT, NS, host and PTR records only. Two consequences today:

  1. AAAA records already present in a zone are invisible to Records().
  2. Any AAAA endpoint in a plan is silently dropped by recordSet() — the switch ep.RecordType has no matching case, so it returns a zero value with err == nil. No error surfaces; the record simply never gets created.

So IPv6 services managed through ExternalDNS could not be reconciled against Infoblox at all.

Not included

PTR records are still created for A records only. The reverse-zone path is IPv4-oriented (rfc2317.CidrToInAddr, net.ParseCIDR + Contains, and record:ptr keyed on ipv4addr); ip6.arpa nibble-zone delegation needs separate handling and is better as its own change. Getting it half-right would create broken reverse records.

Testing

internal/infoblox/infoblox_test.go extends the existing mock connector (create/get/delete/update cases, the paging response type, object factories, plus a new host-record factory taking IPv4 and IPv6 address lists) and covers:

  • fetching AAAA records, including the record:aaaa GetObject request and query params, in both the plain and view variants
  • multi-target AAAA and duplicate-target de-duplication
  • dual-stack and IPv6-only host records
  • create / update / delete through ApplyChanges, including in-filter and out-of-filter domains and the reverse/PTR variant

go build, go vet, go test ./... and golangci-lint run ./... (0 issues) all pass; main was verified green beforehand so nothing pre-existing is attributed here.

The new tests were mutation-checked to confirm they actually assert the new behaviour: removing the AAAA fetch, the de-duplication guard, or the recordSet AAAA case each makes them fail.

Also adds AAAA to the supported-records table in the README.

Context

We maintain a fork of external-dns carrying an equivalent AAAA patch against the old in-tree Infoblox provider (removed upstream in v0.15.0). Contributing it here instead lets us drop that fork and consume stock upstream external-dns — which is why the implementation deliberately follows this repo's conventions rather than porting the old provider's shape.

MaxRink added a commit to MaxRink/external-dns-infoblox-webhook that referenced this pull request Aug 6, 2026
…ranch

GitHub only registers a workflow, and therefore only evaluates its
push-tag trigger, once the file has been seen on the repository default
branch. tcaas-image.yaml existed solely on tcaas-main, so it was never
registered: actions/workflows lists only the four upstream workflows and
three -telekom.N tag pushes produced zero runs.

Add the file to main so the trigger registers. Also add workflow_dispatch
with an explicit ref input for on-demand rebuilds, and a guard that
refuses to publish unless the resolved ref is a v*-telekom.* tag, so a
dispatch from main cannot publish a misleading image tag.

This file is fork-only and is deleted together with the rest of the
bridge once upstream ships PRs AbsaOSS#69/AbsaOSS#70.
MaxRink added a commit to MaxRink/external-dns-infoblox-webhook that referenced this pull request Aug 15, 2026
PR AbsaOSS#69 added AAAA records but deliberately left PTR creation to A records
only, on the assumption that the reverse zone lookup was IPv4 specific.
It turns out most of that machinery is already family agnostic:

- rfc2317.CidrToInAddr already derives ip6.arpa nibble names for IPv6
  CIDRs ("2001:db8::/64" -> "0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"),
  so Records() needs no change to fetch PTR records from an IPv6 reverse
  zone.
- findReverseZone uses net.ParseCIDR and net.IPNet.Contains, both of
  which already match IPv6 addresses against IPv6 reverse zones.

What actually had to change:

- ChangesByZone now derives the companion PTR change for AAAA endpoints
  as well as A endpoints, and AdjustEndpoints / Records() track the
  infoblox-ptr-record-exists property for AAAA endpoints too.
- recordSet() addresses a PTR record through ipv6addr instead of
  ipv4addr when the target is an IPv6 address, both when building the
  object and when looking the existing object up. A record:ptr carries
  its target in exactly one of the two fields, never in both.
- ToPTRResponseMap() falls back to Ipv6Addr when Ipv4Addr is empty, so
  PTR records read back from an ip6.arpa zone yield a target instead of
  an empty string.

CreatePTR still gates all of this, so nothing changes for users who do
not opt in. As before, the reverse zone has to be part of the
DOMAIN_FILTER; the README documents the IPv6 CIDR form.
MaxRink added a commit to MaxRink/external-dns-infoblox-webhook that referenced this pull request Aug 28, 2026
PR AbsaOSS#69 added AAAA records but deliberately left PTR creation to A records
only, on the assumption that the reverse zone lookup was IPv4 specific.
It turns out most of that machinery is already family agnostic:

- rfc2317.CidrToInAddr already derives ip6.arpa nibble names for IPv6
  CIDRs ("2001:db8::/64" -> "0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"),
  so Records() needs no change to fetch PTR records from an IPv6 reverse
  zone.
- findReverseZone uses net.ParseCIDR and net.IPNet.Contains, both of
  which already match IPv6 addresses against IPv6 reverse zones.

What actually had to change:

- ChangesByZone now derives the companion PTR change for AAAA endpoints
  as well as A endpoints, and AdjustEndpoints / Records() track the
  infoblox-ptr-record-exists property for AAAA endpoints too.
- recordSet() addresses a PTR record through ipv6addr instead of
  ipv4addr when the target is an IPv6 address, both when building the
  object and when looking the existing object up. A record:ptr carries
  its target in exactly one of the two fields, never in both.
- ToPTRResponseMap() falls back to Ipv6Addr when Ipv4Addr is empty, so
  PTR records read back from an ip6.arpa zone yield a target instead of
  an empty string.

CreatePTR still gates all of this, so nothing changes for users who do
not opt in. As before, the reverse zone has to be part of the
DOMAIN_FILTER; the README documents the IPv6 CIDR form.
The provider previously fetched and reconciled only A, CNAME, TXT, NS,
host and PTR records, so IPv6 services managed through ExternalDNS could
never be reconciled against Infoblox: AAAA records already present in a
zone were invisible to Records() and any AAAA endpoint in a plan was
silently dropped by recordSet() because the switch had no matching case.

This adds AAAA end-to-end, following the existing patterns for A records:

- ToAAAAResponseMap() maps []ibclient.RecordAAAA onto the shared
  ResponseMap using the ibclient.AaaaRecord type constant.
- Records() fetches record:aaaa per zone through PagingGetObject, the
  same way A records are fetched.
- ToHostAAAAResponseMap() surfaces the Ipv6Addrs of host records as AAAA
  endpoints, mirroring how Ipv4Addrs are already surfaced as A endpoints.
  Both host mappers now skip records that carry no address of the
  respective family, so an IPv4-only or IPv6-only host record no longer
  yields an endpoint with an empty target.
- recordSet() builds a RecordAAAA for create/update/delete and looks the
  existing object up by name and ipv6addr.
- getRefID() resolves the object reference and log fields for RecordAAAA.

Infoblox can return the same IPv6 address more than once for a name (the
same address in a different but equivalent notation). Such duplicates
would produce a permanently dirty plan, so ToAAAAResponseMap de-duplicates
targets via the new ResponseDetails.Contains helper.

PTR records for AAAA are deliberately not created: the reverse zone
lookup path is built around IPv4 (rfc2317.CidrToInAddr) and ip6.arpa
delegation would need separate handling. CreatePTR therefore keeps
applying to A records only.
ToAAAAResponseMap skips a target that the response map already holds,
to avoid a permanent diff in the plan. The comment says that this also
covers the same address in a different notation, but the comparison was
a plain string comparison, so it did not.

Compare the canonical form of an IP target instead. Targets that are
not IP addresses keep the plain string comparison.
@MaxRink
MaxRink force-pushed the feat/aaaa-record-support branch from 0641210 to 4f534b1 Compare August 28, 2026 01:34
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.

support for IPV6/Dual Stack

1 participant