Skip to content

Add ILNP, AMTRELAY, DRIP, HIP, SIG and PX records - #152

Merged
NelsonVides merged 8 commits into
mainfrom
rfc-record-types
Oct 9, 2026
Merged

NelsonVides merged 8 commits into
mainfrom
rfc-record-types

Conversation

@NelsonVides

@NelsonVides NelsonVides commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Adds the ILNP (NID, L32, L64, LP), AMTRELAY, HHIT and BRID, HIP, SIG and PX record types, on the wire, in zone files and in JSON, and releases them as v5.1.0.

Each type decodes from and encodes to the wire, parses from and encodes to zone files, and transcodes to JSON, with load-time checks in dns_check and DNSSEC canonical forms per RFC 4034 §6.2. Names inside the new RDATA are never compressed on the wire. Where the RFCs leave RDATA undefined (an AMTRELAY relay type past 3, a HIP with an empty HIT or public key), it stays opaque, as for an unknown type, and round-trips through JSON as base64. SIG covers the record format, and so SIG(0) message signatures as records; creating and verifying SIG(0) signatures is not part of this.

Two behavior changes for consumers, also in the changelog:

  • RRs of types SIG (24), PX (26) and HIP (55) used to decode as opaque binary RDATA and now decode into records, so code that matches a binary data for them no longer matches. As for every type decoded into a record, an empty RDATA for them now decodes to formerr.
  • Zone files: as for the existing types, a line that starts with one of the new mnemonics and has no class reads as a record of that type with a blank owner. An owner spelled as one of them, such as a host named PX, needs a class after it, as in PX IN A 192.0.2.1.

Known limitation, not new here: in zone files, a base64 or hex piece made only of digits (1234, 0012, 12h) is lexed as a number, so it fails for HHIT and BRID as it already does for DNSKEY, RRSIG and TLSA. Fixing it needs a lexer change for every type, left for a follow-up.

🔍 QA

Scenario: Test suite

  • Run make test: fmt, lint, xref, dialyzer and all 803 CT tests pass, with coverage at 93%.

Scenario: Consumers

  • Before bumping erldns to this release, check it does not match SIG, PX or HIP RRs by a binary data.

📋 Deployment Pre/Post tasks

  • After merging, tag the merge commit on main as v5.1.0 and push the tag to publish to Hex.pm.

:shipit: Deployment Verification

  • v5.1.0 is published on Hex.pm.

@NelsonVides NelsonVides self-assigned this Oct 5, 2026
@NelsonVides NelsonVides added the enhancement New feature, enhancement or code changes, not related to defects label Oct 5, 2026
Base automatically changed from fix/rfc-conformance to main October 6, 2026 10:52
@NelsonVides
NelsonVides force-pushed the rfc-record-types branch 2 times, most recently from 5b5d0a5 to 409f1c5 Compare October 6, 2026 12:21
NID (104), L32 (105), L64 (106) and LP (107) decoded as opaque bytes,
failed to parse from a zone file and had no JSON form. They now get
records of their own: a 16-bit preference with a 64-bit NodeID, a
32-bit Locator32, a 64-bit Locator64 or a FQDN.

All four are class independent (RFC 6742 §2), so unlike A and
EUI48/EUI64 they decode in any class. LP's FQDN is written without
compression (§2.4) and keeps its case in DNSSEC canonical form, since
LP is not one of the types RFC 4034 §6.2 lowercases (RFC 3597 §7).

The NodeID and Locator64 are 8-byte binaries, as the EUI64 address is,
and like it the encoder refuses one of another length, which it would
write under an RDLENGTH of 10 and so shift every record after it.
dns_check refuses the same at load time, along with a preference past 16
bits, a Locator32 that is not an address and an LP name that does not
fit: dns_json:from_map/1 raises {invalid_record, Record} and the zone
file parser returns a semantic error.

In zone files a NodeID or Locator64 is four colon-separated groups of
1-4 hex digits, and is written back with all four digits of each group.
The "::" shorthand is rejected (§2.1, §2.3). The Locator32 goes through
the same strict parser as an A record, so leading zeros are refused,
including in the RFC's own "10.1.02.0" example. In JSON the 64-bit
values are hex, as EUI64 is, and the Locator32 is a dotted quad.

Tests cover known-answer wire vectors built from the RFC examples, the
class independence, the uncompressed LP target, the encoder's refusal of
a NodeID or Locator64 of the wrong size, the load-time checks for the
8-byte values and field widths, zone parse, encode and round trip, JSON
transcoding and rejection, the canonical form, and the four types in the
wire and zone-file properties. fmt, lint, xref, dialyzer and ct (760)
pass, and cover stays at 92%.
AMTRELAY (260) decoded as opaque bytes, failed to parse from a zone file
and had no JSON form. It now gets a record of its own: an 8-bit
precedence, the D-bit as discovery_optional, the relay type and the
relay, which is empty for type 0, an IPv4 or IPv6 address for types 1
and 2, and a domain name for type 3.

AMTRELAY is class independent (§4.1), so unlike A and AAAA its addresses
decode in any class. The relay's name is written without compression
(§4.2.3) and keeps its case in DNSSEC canonical form, as AMTRELAY is not
one of the types RFC 4034 §6.2 lowercases (RFC 3597 §7). Relay types 4
to 127 are undefined and receivers SHOULD NOT use them (§4.2.3), so
their RDATA stays opaque, as BIND keeps it, and so does a relay of the
wrong length for types 0 to 2. A type 3 relay that is not one name,
being missing, cut short or followed by more bytes, makes the message a
FORMERR, as a bad name in any other RDATA does.

The encoder trusts the record, so dns_check refuses at load time a relay
that is not what its type announces, an undefined relay type, a D-bit
that is not a boolean and a precedence past 8 bits. A type 0 record
carries no relay on the wire, so one holding a name would have lost it.

In zone files the relay is "." for type 0, and a type 3 relay is a name
even when it reads like an address, as in BIND. In JSON the relay is an
address string, a name or the empty string; the address is parsed after
the record is built, since it depends on the relay type.

Tests cover known-answer vectors from the RFC 8777 §4.3.2 examples, as
corrected by erratum 6218, the class independence, the uncompressed
relay, the opaque undefined types, the FORMERR for a bad type 3 relay,
the load-time checks, zone parse, encode and round trip, JSON
transcoding and rejection, the canonical form, and AMTRELAY in the wire
and zone-file properties. fmt, lint, xref, dialyzer and ct (772) pass,
and cover stays at 92%.
HHIT (67) and BRID (68) decoded as opaque bytes, failed to parse from a
zone file and had no JSON form. Each now gets a record holding its data,
the CBOR of a DRIP Entity Tag's registration (§5.1) or of a drone's
static Broadcast Remote ID (§5.2), carried as it is. The CBOR is not
checked, as BIND and PowerDNS do not check it either, and neither type
is bound to a class.

The encoder trusts the record, so dns_check refuses at load time data
that is empty or not a binary. The CBOR has mandatory fields, and the
decoder refuses RDATA of zero length for every type it knows, so an
empty record would have gone out and not come back.

In zone files the data is base64, which may be split into any number of
whitespace-separated pieces, also across lines in parentheses (§5.1.1,
§5.2.1), and it is written back as a single string. In JSON it is
base64, as OPENPGPKEY's data is.

Tests cover the RFC's Appendix A.2.2 zone file, with the BRID data that
erratum 8823 corrects, checked against the CBOR structure of Figures 19
and 21, the data carried as it is in any class, the load-time check for
empty data, zone parse, encode and round trip, JSON transcoding and
rejection, and both types in the wire and zone-file properties. fmt,
lint, xref, dialyzer and ct (779) pass, and cover stays at 92%.
HIP (55) decoded as opaque bytes, failed to parse from a zone file and
had no JSON form. It now gets a record of its own: the public key
algorithm, the HIT, the public key and the rendezvous servers, a list
of names that may be empty. The HIT and key lengths are not fields, as
they are the lengths of the binaries.

The rendezvous servers' names are written without compression (§5.6)
and keep their case in DNSSEC canonical form, as HIP is not one of the
types RFC 4034 §6.2 lowercases (RFC 3597 §7). HIP is not bound to a
class, as BIND has it. The HIT and the public key are REQUIRED (§5), so
a HIP RDATA with either empty, which BIND refuses, stays opaque.

The encoder refuses a HIT past 255 bytes, which would go out with a
wrapped 8-bit length, and dns_check refuses it at load time too, along
with an empty HIT or key, an algorithm past 8 bits and servers that are
not a list of names that fit.

In zone files the HIT is hex and the key base64, each one word, as §6
has them, followed by the servers, which may be relative to the origin.
In JSON the HIT is hex and the key base64. A HIT of decimal digits only
lexes as a number and loses its leading zeros; that is the zone file
lexer's limitation, which DS digests share, and the zone property draws
HITs and keys that lex as words, as it does HHIT and BRID data.

Tests cover the RFC's §7 example as a known-answer wire vector in any
class and as a zone file, the uncompressed server names, the opaque
RDATA without a HIT or key, the encoder's refusal of a HIT past 255
bytes, the load-time checks, zone parse and rejection, each case
breaking one field of a record that parses, encode and round trip, JSON
transcoding and rejection, the canonical form, and HIP in the wire and
zone-file properties. fmt, lint, xref, dialyzer and ct (789) pass, and
cover stays at 92%.
SIG (24) decoded as opaque bytes, failed to parse from a zone file and
had no JSON form. It now gets a record of its own, with RRSIG's fields:
RRSIG took over SIG's RDATA for zone data (RFC 4034 §3), and SIG lives
on as SIG(0), which signs a whole message with type covered 0 and goes
last in the additional section (RFC 2931 §3). Only the record comes
with this; creating and verifying SIG(0) signatures does not.

SIG is not one of RFC 1035's types, so the signer's name is written
without compression (RFC 3597 §4), and it is lowercased in DNSSEC
canonical form, as SIG is on the list of RFC 4034 §6.2. The algorithm
is any 8-bit number, since SIG predates the DNSSEC algorithms RRSIG's
record lists.

The encoder trusts the record, so dns_check refuses at load time a field
past its width and a signer's name that does not fit, as it does for
RRSIG. In zone files and JSON a SIG is spelled as an RRSIG, which it
shares the presentation format with (RFC 2535 §7.2), the times as
seconds or YYYYMMDDHHMMSS dates and the signature in base64; the zone
file parser and encoder go through RRSIG's. A SIG(0) is class ANY, which
zone files do not take, so one the zone encoder writes does not read
back.

The record has RRSIG's fields in the same order, so the wire codec,
dns_check and the zone file parser and encoder read and write a SIG as
an RRSIG and retag it, rather than each keep a copy of RRSIG's code that
a fix to one could miss.

Tests cover a known-answer wire vector, SIG and RRSIG having the same
fields and staying opaque alike, a SIG(0) at the end of a message's
additional section, with class ANY and the uncompressed signer's name,
the load-time checks, zone parse with both time forms and a TYPE0 type
covered, encode and round trip, JSON transcoding of a SIG(0) and
rejection, the lowercased canonical form, and SIG in the wire
properties. fmt, lint, xref, dialyzer and ct (796) pass, and cover is at
93%.
PX (26) decoded as opaque bytes, failed to parse from a zone file and
had no JSON form. It now gets a record of its own: a preference and the
two names that map between Internet and X.400 mail domains, MAP822 and
MAPX400.

PX is defined for class IN (§4), so like A it decodes in IN and NONE
only and stays opaque in other classes. PX is not one of RFC 1035's
types, so its names are written without compression (RFC 3597 §4),
and they are lowercased in DNSSEC canonical form, as PX is on the list
of RFC 4034 §6.2.

The encoder trusts the record, so dns_check refuses at load time a
preference past 16 bits and names that do not fit. In zone files both
names may be relative to the origin; in JSON they are plain names.

Tests cover a known-answer wire vector from the §4.1 examples and its
uncompressed names in a message, the class binding, the load-time
checks, zone parse of the §4.1 examples, encode and round trip, JSON
transcoding and rejection, the lowercased canonical form, and PX in the
wire and zone-file properties. fmt, lint, xref, dialyzer and ct (802)
pass, and cover stays at 93%.
The ILNP, AMTRELAY, DRIP, HIP, SIG and PX records are new features, so
their changelog entries go under a minor release, v5.1.0. main is left
empty above it.
JSON: a type this library decodes can still leave RDATA opaque, as an
AMTRELAY with an undefined relay type or a HIP with an empty HIT does,
and to_map writes that as a lone base64 "data" key. from_map built a
record from it and refused it. A lone "data" key for a record without a
data field now loads back as the opaque binary.

SIG was read and written as an RRSIG by swapping the record tag with
setelement, which held only while both records kept their fields in the
same order. dns_encode now converts between them field by field with
sig_to_rrsig/1 and rrsig_to_sig/1, which every codec uses, and
dns:rrdata_sig() names the record's type.

AMTRELAY is encoded by one clause, its relay by relay type in a helper.
The NID and L64 zone builders share one, and an ILNP NodeID or
Locator64 is read with hex_to_binary/1 rather than a hand-written
digit check.

The changelog now says that SIG, PX and HIP RRs, which decoded as
opaque binaries, now decode into records, and that a zone line starting
with one of the new mnemonics and no class reads as a record of that
type, as it does for the other types.

make test passes: fmt, lint, xref, dialyzer, ct (803) and cover (93%).
@NelsonVides
NelsonVides merged commit 91984be into main Oct 9, 2026
6 checks passed
@NelsonVides
NelsonVides deleted the rfc-record-types branch October 9, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature, enhancement or code changes, not related to defects

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant