Monday, January 23, 2023
A few small differences
I received the following patch for my DNS library:
I am hoping to use this library to encode and decode mDNS queries and responses. It seems that the mDNS is mostly the same as unicast DNS, except for a few small differences which I aim to add to this PR as I encounter them.
Mdns mods by oviano · Pull Request #13 · spc476/SPCDNS
Those “few small differences” turn out not to be so small.
The main RFCs for mDNS appear to be RFC-6762 and RFC-6763 and to support them in full requires breaking changes to my library. The first are a bunch of flags, defined in RFC-6762 and it affects pretty much the entire codebase. The first deals with “Questions Requesting Unicast Responses.” Most flags are defined in the header section, but for this, it's “the top bit in the class field of a DNS question as the unicast-response bit.” And because mDNS specifically allows multiple questions, it's seems like it could be set per-question, and not per the request as a whole, as the RFC states: “[w]hen this bit is set in a question, it indicates that the querier is willing to accept unicast replies in response to this specific query, as well as the usual multicast responses.” To me, that says, “each resource record needs a flag for a unicast reponse.” The other bit the “outdated cache entry” bit. which again applies to individual resource records and not to the request as a whole. And again, to me, that says, “each resoure record needs a flag to invalidate previously cached values.”
How to handle this … well, one way would be to a Boolean field to each resource record type to hide protocol details (which was the point in this library frankly). But that can break existing code as the new fields will need initialization:
dns_question_t domain; domain.name = host; domain.type = RR_A; domain.class = CLASS_IN; domain.uc = true; /* we want unicast reply */ /* and the other flag */ dns_a_t addr; addr.name = host; addr.type = RR_A; addr.class = CLASS_IN; addr.ttl = 0; addr.ic = true; /* invalidate cache data */ addr.address = address;
and document that the uc and ic fields are for mDNS use;
if you aren't using mDNS,
then they should be set to false
.
Another approach is to leak protocol details and require the user to do something like:
/* We're making a query and want a unicast reply */ dns_question_t domain; domain.name = host; domain.type = RR_A; domain.class = CLASS_IN | UNICAST_REPLY; /* We're replying to a query and want to invalidate this record */ dns_a_t addr; addr.name = host; addr.type = RR_A; addr.class = CLASS_IN | INVALIDATE_CACHE; addr.ttl = 0; addr.address = address;
And that's a less-breaking change, but on the decoding side, I still need some form of flag in the structure to indicate these flags were set because otherwise data is lost.
I'm not sure which approach is best. The first does a better job of hiding the DNS protocol details, but breaks more code. The second is less breaking, as I could ignore any cache flags on encoding, but it leaks details of DNS encoding to user code. I tend to favor the first but I really dislike the breaking aspect of it. And That's just the first RFC.
The other RFC utilizes what I consider to be an implementation detail of the DNS protocol to radically alter how I handle text resource records. The RFC that defined modern DNS, RFC-1035, describes the format for a text resource record, but is silent as to semantics.
Individual resource records come with a 16-bit length, so in theory, a resource record could be up to 65535 bytes in size, but it's rare to get a record that size. The base type of a text resource record is a “string.” and RFC-1035 defines a “string” as one byte for the length, followed by that many bytes as the contents. The length of a “string” is defined as one byte, which limits the length of 255 bytes in size. This means, in practice, that a text resource record can contain several “strings.”
How SPCDNS handles this now is that I assume a text resource record only has one value—a string:
typedef struct dns_txt_t /* RFC-1035 */ { char const *name; dns_type_t type; dns_class_t class; TTL ttl; size_t len; char const *text; } dns_txt_t;
When encoding such a record, I break the given string into as few DNS “strings” as possible. Give this a 300 byte string, and you get two DNS “strings” encoded, one being 255 byte long, and the other one 45 bytes long. Upon decoding, all the strings in a single text resource record are concatenated into a single string. As I said, DNS-1035 doesn't go into the semantics of a text resource record, and I did what I felt was best.
RFC-6763 uses the DNS “string” encoding for semantic information:
Apple TV - Office._airplay._tcp.local. 10 IN TXT ( "acl=0" "btaddr=00:00:00:00:00:00" "deviceid=A8:51:AB:10:21:AE" "fex=1d9/St5/FbwooQ" "features=0x4A7FDFD5,0xBC157FDE" "flags=0x18644" "gid=F014C3FF-1420-4374-81DE-237CD6892579" "igl=1" "gcgl=1" "model=AppleTV14,1" "protovers=1.1" "pi=c6fe9e6e-cec2-44c8-9c66-8994c6ad47" "depsi=4A342DB4-3A0C-47A6-9143-9F6BF83F0EDD" "pk=5ab1ac3988a6a358db0a6e71a18d31b8d525ec30ce81a4b7b20f2630449f6591" "srcvers=670.6.2" "osvers=16.2" "vv=2" )
I have to admit, this is ingenious—each DNS “string” here defines a name/value pair. But I did not see this use at all.
I wonder how much code out there dealing with DNS packets (not specifically mDNS) would treat these records:
IN TXT "v=spf1 +mx +ip4:71.19.142.20/32 -all"
IN TXT "google-site-verification=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
the same way as:
IN TXT (
"v=spf1 +mx +ip4:71.19.142.20/32 -all"
"google-site-verification=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
)
The first returns two text resource records, each consisting of a single DNS “string,” the second one text resource record but with two DNS “strings.” My gut feeling is “not many would deal with the second format” but I can't know that for sure.
And changing how I deal with text resource records in SPCDNS would be a major breaking change.
This is one change I really don't know how to approach.