Skip to content

fix: write A records as host-record so CNAMEs resolve - #2

Open
doonga wants to merge 1 commit into
TheOutdoorProgrammer:mainfrom
doonga:fix/host-record-for-cname-targets
Open

fix: write A records as host-record so CNAMEs resolve#2
doonga wants to merge 1 commit into
TheOutdoorProgrammer:mainfrom
doonga:fix/host-record-for-cname-targets

Conversation

@doonga

@doonga doonga commented Aug 18, 2026

Copy link
Copy Markdown

Problem

A CNAME created by this webhook does not resolve when its target is an A record that the webhook also created.

dnsmasq resolves cname=alias,target only when target is a name it already knows from host-record, /etc/hosts, or a DHCP lease. A name defined with address=/name/ip is not a valid CNAME target, so the alias silently fails to resolve even though the A record itself answers correctly.

This affects any setup where external-dns publishes an A record for a gateway or ingress and then points service hostnames at it with CNAMEs — a common pattern with Gateway API and --source=gateway-httproute.

Fix

Write A records as host-record=name,ip rather than address=/name/ip.

The reader still parses the legacy address=/name/ip form, so external-dns can continue to see, reconcile, and delete records written by earlier versions.

Behavior changes

Two differences worth being explicit about:

  • host-record matches only the exact name, where address= additionally matched every subdomain of it. Exact matching is the correct semantic for external-dns, which manages exact names — but it is a change for anyone who was relying on the implicit wildcard.
  • host-record also creates a PTR record, so reverse lookups for these names now resolve.

Migration

Existing address= files are rewritten to host-record the next time external-dns changes that record. A record that never changes keeps the old form — and its CNAMEs keep failing — until something touches it.

Testing

Added test-host-record.js, following the existing test-*.js convention (standalone script, no runner required):

$ node test-host-record.js
Testing dnsmasq record formats...
PASS: A record uses host-record
PASS: multiple A targets each get a host-record line
PASS: CNAME record unchanged
PASS: TXT record unchanged
PASS: host-record reads back as A
PASS: multiple host-record lines read back as one record
PASS: cname reads back as CNAME
PASS: legacy address= still reads back as A
Record format tests completed with 0 failure(s).

It exercises the real service against a temp directory, covering both the formats written and the formats read back, including a legacy address= file to prove backward compatibility. It exits non-zero on failure, so it can be wired into CI if you ever want that.

writeRecord is exported so the test can cover record formatting directly. applyChanges ends by restarting firerouter_dns, which a test has no business doing on a Firewalla — calling writeRecord avoids that entirely.

Also verified end to end on a live Firewalla with external-dns driving the webhook: A records land as host-record, and CNAMEs pointing at them now resolve through to the address (hostnames redacted):

$ dig +short ingress.example.com
10.0.20.50                          # A record, written as host-record

$ dig +short service.example.com
ingress.example.com.                # CNAME whose target is that A record
10.0.20.50                          # ...which now resolves through

Before the change the second lookup returned only the CNAME, with no address behind it.

dnsmasq only resolves a `cname=` whose target is a name it already knows
from host-record, /etc/hosts or a DHCP lease. Names defined with
`address=/name/ip` are not valid CNAME targets, so any CNAME pointing at
an A record created by this webhook failed to resolve.

Write A records as `host-record=name,ip` instead. The reader still parses
the legacy `address=/name/ip` form so external-dns can reconcile and remove
records written by earlier versions.

Note that host-record matches only the exact name, where address= also
matched every subdomain, and host-record additionally creates a PTR record.

Adds test-host-record.js covering both directions: the formats written for
A, CNAME and TXT records, and reading back host-record, cname, txt-record
and legacy address= entries. It calls writeRecord — now exported — rather
than applyChanges, so the test never restarts firerouter_dns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant