Magic DNS#
Fri Jan 2 09:58:45 2026
It's all very 2026 around here, isn't it? I am reminded by
to jot down some of what I've been doing in the past month or so. The tl;dr is "making a thing I can plug into my motorbike ECU to log the data (rpm, speed, throttle position, temperatures etc etc) it produces". For reasons mostly of ramifying the learning opportunities, I decided the best way would be to get a cheap ESP-32 device (it's RISC-V - isn't that cool?) and hook it up to a level converter, and then write a program for it in Rust (Rust learning opportunity ahoy) to twiddle the serial line appropriately and send the data over the network to the mobile phone which sits on my handlebars.
It turns out that I spent way less time getting the serial interface to the Honda K-line ECU signal to reveal its secrets than on the "why don't you just ..." part where I want to stream the data over wifi to another device. So this post is actually not at all about hardware hacking.
The constraints I have imposed on myself here are
-
I do not want to hardcode my wifi ssid and password into the device (actually, I have at least two wifi networks I may want to use it with)
-
the (yet to be written) data collection app should be able to find the device without hardcoding its IP address or requiring me to type it in - as the device is getting its address from DHCP, we don't even know what address it will get
These are both in principle solved problems.
There's a convention for provisioning wifi on these devices which involves using a mobile phone app to connect to it using BLE then sending the ssid/password of the chosen access point. In fact there's even a prebuilt Android app which we can use and an esp32 arduino library which we can't (because we have elected to make our lives difficult and use Rust instead). But I am led to believe that "rewrite everything in Rust" is idiomatic for Rust programmers anyway. I haven't done this yet.
And for the "what's my IP address" problem there is a standard way, by combining Multicast DNS and DNS-based Service Discovery, for computers to publish their services on the LAN. When I say "computers": if this household is typical, mostly they're set-top boxes, printers, light bulbs, smart speakers and thermostats rather than general-purpose computing devices. I've mostly done this bit.
Terms
Multicast DNS is DNS, but peer-to-peer: it reuses mostly the same packet formats but instead of requiring a centralised server which knows all the names, every device listens on a multicast address for DNS queries for its own name.
DNS-SD is a convention for which records you can query/need to send in order to advertise what kind of services you have and where they are. Because sending an A record alone is not sufficient for anyone with a Mac and a fancy-schmancy service browser to know what kind of service is on offer at that address. Is it a printer? A dishwasher? An IoT air fryer?
The RFCs for each (which are, by the way, much easier reads than a lot of RFCs and contain no EBNF at all) go to great lengths to point out that each is independent of the other. But they stack well.
DNS-SD, 3048 metre view
DNS-SD is based on a paradigm of "services" and "service instances". A
"service" is the general "kind" of thing on offer and is named
something like _http._tcp.local - it will always end in _tcp.local
if it is TCP or _udp.local if it is anything other than TCP. For our
ECU project we chose the service name _keihin._udp.local after the
manufacturer of the ECUs that the device knows how to talk to. A
service instance might be something like
WiserHeat05AB12._http._tcp.local. Service names aren't usually
hierarchical but there are a few with a second level like
_printer._sub._http._tcp
The minimum/usual set of records you need to publish for DNS-SD is this (pseudocode)
myinstancename._theservicename._udp.local SRV, data: (target: myhostname.local, port: nnnn)
myhostname.local A, data: a.b.c.d
myinstancename._theservicename._udp.local TXT, data: "txtvers=1"
_theservicename._udp.local PTR, data: myinstancename._theservicename._udp.local
_services._udp.local PTR, data: _theservicename._udp.local
Your service instance needs a SRV and a TXT, then there's a PTR
connecting the service instance to the service for people who are
browsing the service - think about e.g. an "Add a printer" dialog box,
then there's a PTR from _services._udp.local to the service name PTR
for people who are running avahi-browse -a or its moral equivalent
in GUI-land. And not forgetting there's an A record matching the one in the
SRV record data.
-
Note:
_services._udp.localis the right name for discovery even if your service is TCP - there is no_services._tcp.local -
Note: I am assuming
.localis the suffix, which is likely true for MDNS but probably not if you are using DNS-SD with regular DNS
MDNS
The single biggest problem when implementing MDNS is the lack of tooling to test it against. In my experience:
-
dig: historically, you used to be able to
dig @224.0.0.251 -p 5353 name.localbut it largely worked by accident and now it doesn't. Note that even when it did work it wasn't sending the same packets as a real MDNS query. -
avahi-browse: e.g.
avahi-browse -v -ashows all the services on the LAN. Note that is is a frontend to the persistentavahi-daemonand there is caching happening in there somewhere, so if it didn't work then and you made a change and restarted your service ... is your service still broken or did it not reissue the query? shrug-emoji.gif -
mqueryfrom Jeremie Miller's mdnsd: note that this has been forked into a million pieces. The one I'm using is https://github.com/troglobit/mdnsd. This works for querying the_services._udp.localdiscovery name but if you run e.g.mquery _scanner._tcp.localit appears to send the query and then sit there silently ignoring the responses. But: it doesn't cache as avahi-browse does, so that's good. -
wireshark, of course. Wireshark is pretty good, but note that it will display your replies as "Unsolicited" because the query id for MDNS is (per the standard) 0, so there is no way for it to correlate them with requests.
-
mdns-debugger was handy for pointing out my TTLs (and a lot of other TTLs) were wrong. It didn't point out that my PTR record data was incorrectly encoded and was therefore naming a nonexistent A record, which was a source of much hair tearing.
-
there are a couple of Android apps I also used, mostly to see what they'd do when nothing was working (see "hair tearing" above) and I wa sout of ideas. "mDNS Discovery" (com.mdns_discovery.app) and "Service Browser" (com.druk.servicebrowser). The first one is prettie, the second one very helpfully rendered the errant PTR as with a backslash as
eculogical\.localand so led me to the said encoding error.
Where are we now?
I believe that it now does everything an mdns responder SHOULD(sic) do except
- compress labels in response data
- ignore queries for records where the record we'd send is already in the answers section of the query message
- NSEC
and I can't decide, in the context of this being a program that probably nobody else in the world will ever use and even I will only use on one single piece of hardware (I only have one motorbike) whether implementing those things is a good and laudable decision because spec compliance is important, or just a way of further putting off the inevitable next step which involves writing the Android app to collect the data.
It also could do with being extracted into its own module/crate/thing to be more modular. I'd say "to aid reuse" but I don't think anyone really wants to (or should want to) reuse my novice-level Rust code. Learning in public.