plan::build compares desired against stored on deSEC's wire form. convert.rs reproduces two
axes of deSEC's normalization: embedded domain names (rdata_name_field) and TXT quoting
(is_text_type). deSEC normalizes more than that:
Record values that are not given in canonical form, such as 0:0000::1 for an IPv6 address, will
be converted by the API into canonical form (here: ::1). [...] exact validation and
canonicalization depend on the record type.
deSEC docs, RRsets
Where deSEC canonicalizes a value we do not reproduce, the comparison reports a difference that is
not one, and we write. Every cycle, against a budget of 300 per zone per day. That is the write
loop this provider exists to prevent, arriving through the storage path instead of the adjust path.
Why nothing is currently on fire
The two obvious instances are absorbed, both for reasons that are load-bearing and recorded nowhere.
Long TXT. deSEC splits: "Token strings longer than 255 characters are automatically split into
several token strings." We send one chunk and get several back, so the internal comparison genuinely
does differ. It never fires because external-dns never asks: /records reports txt_unquote, which
joins the chunks back into one target.
IPv6 compression. We do nothing here at all. external-dns absorbs it: Targets.Same falls back
to netip.ParseAddr and compares string forms when EqualFold fails.
What is left
Types where deSEC canonicalizes, external-dns's Same does not absorb it (not case-only, not an
IP), and our egress does not map it back. LOC, SVCB/HTTPS parameter ordering and quoting, and
whatever comes next. Narrow, possibly empty in practice, and currently invisible: nothing notices
that an RRset was written and came back different.
adjusting_storing_and_reading_back_is_a_fixpoint cannot catch it, because it round-trips through
our own records_for rather than through a canonicalizing server. docs/testing.md claims the two
property tests make the no-op write loop structurally impossible; that holds for the adjust path
only.
Two claims to correct either way
- docs/testing.md calls the TXT split "the open question only the live layer can answer" and argues
"we emit a single chunk and store whatever the server returns, so the split should never matter".
deSEC documents the split, so the question is answered; and the argument is wrong, since storing
the server's form is exactly what makes the internal comparison differ. The reason it does not
matter is the egress join.
txt_quote's doc comment says letting deSEC do the splitting "is what keeps our idea of the split
from ever disagreeing with its own". The two do disagree; nothing ever compares them.
Options
- Detect it.
write_zone already holds both sides, plan.patches and confirmed. Comparing them
costs almost nothing and turns the whole open-ended class into one signal, say
webhook_write_normalized_total{record_type}, plus a log line.
- Suppress it. Remember, per RRset, which desired form produced the current stored form; a desired
form we have already written is identical even when the stored bytes differ. General and
self-teaching, carried through store::publish.
- Record the external-dns dependency in docs/protocol.md, with a test, so a future change to
Targets.Same becomes a known risk rather than a mystery.
- Extend
just live-test with long TXT, LOC and HTTPS round trips.
Building 2 before 1 would be writing a mechanism for a failure not yet observed.
plan::buildcompares desired against stored on deSEC's wire form.convert.rsreproduces twoaxes of deSEC's normalization: embedded domain names (
rdata_name_field) and TXT quoting(
is_text_type). deSEC normalizes more than that:deSEC docs, RRsets
Where deSEC canonicalizes a value we do not reproduce, the comparison reports a difference that is
not one, and we write. Every cycle, against a budget of 300 per zone per day. That is the write
loop this provider exists to prevent, arriving through the storage path instead of the adjust path.
Why nothing is currently on fire
The two obvious instances are absorbed, both for reasons that are load-bearing and recorded nowhere.
Long TXT. deSEC splits: "Token strings longer than 255 characters are automatically split into
several token strings." We send one chunk and get several back, so the internal comparison genuinely
does differ. It never fires because external-dns never asks:
/recordsreportstxt_unquote, whichjoins the chunks back into one target.
IPv6 compression. We do nothing here at all. external-dns absorbs it:
Targets.Samefalls backto
netip.ParseAddrand compares string forms whenEqualFoldfails.What is left
Types where deSEC canonicalizes, external-dns's
Samedoes not absorb it (not case-only, not anIP), and our egress does not map it back.
LOC,SVCB/HTTPSparameter ordering and quoting, andwhatever comes next. Narrow, possibly empty in practice, and currently invisible: nothing notices
that an RRset was written and came back different.
adjusting_storing_and_reading_back_is_a_fixpointcannot catch it, because it round-trips throughour own
records_forrather than through a canonicalizing server. docs/testing.md claims the twoproperty tests make the no-op write loop structurally impossible; that holds for the adjust path
only.
Two claims to correct either way
"we emit a single chunk and store whatever the server returns, so the split should never matter".
deSEC documents the split, so the question is answered; and the argument is wrong, since storing
the server's form is exactly what makes the internal comparison differ. The reason it does not
matter is the egress join.
txt_quote's doc comment says letting deSEC do the splitting "is what keeps our idea of the splitfrom ever disagreeing with its own". The two do disagree; nothing ever compares them.
Options
write_zonealready holds both sides,plan.patchesandconfirmed. Comparing themcosts almost nothing and turns the whole open-ended class into one signal, say
webhook_write_normalized_total{record_type}, plus a log line.form we have already written is identical even when the stored bytes differ. General and
self-teaching, carried through
store::publish.Targets.Samebecomes a known risk rather than a mystery.just live-testwith long TXT,LOCandHTTPSround trips.Building 2 before 1 would be writing a mechanism for a failure not yet observed.