The life and times of mDNS: scanning your home network
Published 2016-11-12Although I've given it different names over the years, my goal with Hub has always been a fully-automatic server that anyone can use, but with the option to maintain complete privacy and autonomy.
To that end we provide an automated process for configuring Hub out-of-the-box, but you can also configure it manually using our own setup tool or even generic tools.
Today I'm going to explain how our setup tool uses mDNS (also known as zeroconf and Bonjour and Multicast DNS) to find Hub on your home network.
First I'll give a general introduction to mDNS and then the specifics of ppl-compatible Hub devices.
- For non-geeks
- Q&A over the network
- Who's There?
- I'm Here!
- Hub by ppl
- Resources
For non-geeks
We basically send a few messages back and forth on your home network that, in summary, amount to this:
Dear anyone who speaks mDNS:
Is anyone connected to the router or WiFi a ppl-compatible Hub device?
With Love, ppl Setup App
And Hub will send back a response like this:
Dear ppl Setup App:
Yes, I'm a ppl-compatible Hub device.
With Love, Hub
Okay. Now you should probably run and hide. Continue reading at your own peril.
Q&A over the network
mDNS is a datagram (also known as dgram and udp) protocol that uses broadcast mode and membership groups (at the udp level).
To send and receive its messages the network listener must
be bound to port 5353
, broadcast mode must be enabled
and the socket added to the membership group for 224.0.0.251
.
When an mDNS query is sent out over the network it will usually
be broadcast addressed to 224.0.0.251
, but may also be addressed
to a specific device.
Likewise, the mDNS response (also called an answer) is usually
addressed to 224.0.0.251
but may be addresses to a specific device
The packets sent are actual DNS packets that can be read and written by any library in any language that supports DNS.
node.js example
Just as an example of how this can be done, here it is in node.js:
'use strict';
var dgram = require('dgram');
var dnsjs = require('dns-js');
// SO_REUSEADDR and SO_REUSEPORT are set because
// the system mDNS Responder may already be listening on this port
var socket = dgram.createSocket({
type: 'udp4'
, reuseAddr: true
});
var broadcast = '224.0.0.251'; // mdns
var port = 5353; // mdns
socket.bind(port, function () {
console.log('bound on', port);
// mDNS must listen on the broadcast membership group address
socket.setBroadcast(true);
socket.addMembership(broadcast);
// ... more stuff
});
Who's There?
A program that asks "Who's There?" is called an mDNS Browser. The act of asking is called mDNS Discovery.
Assuming that the computer (laptop, phone, etc) doing the discovery is already connected to WiFi or ethernet on a local network, it needs to send out a packet to ask all devices on the network who's there?
The mDNS standard is designed in such a way that you query a service type and all of the devices that match that service type will respond.
English
"Are there any standardized services on devices on the local network?"
DNS Query
This is shown as the question, not the response
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 0
;; flags: ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;_services._dns-sd._udp.local. IN PTR
Hexidecimal
000000000001000000000000095f7365727669636573075f646e732d7364045f756470056c6f63616c00000c0001
// dns header id, flags, counts (q, ans, auth, add)
0000 0000 0001 0000 0000 0000
// query name
09 5f7365727669636573 07 5f646e732d7364 04 5f756470 05 6c6f63616c 00
// type class
000c 0001
Binary
//
// DNS Header
//
ID: 00000000 00000000
// codes and flags
RD: 0
TC: 0
AA: 0
OPCODE: 0000
QR: 0
RCODE: 0000
CD: 0
AD: 0
Z: 0
RA: 0
// counts
Q: 00000000 00000001
ANS: 00000000 00000000
AUTH: 00000000 00000000
ADD: 00000000 00000000
//
// Question Section:
//
// Name : 9_services7_dns-sd4_udp5local0 => _services._dns-sd._udp.local
// strlen 9
00001001
// str _services
01011111 01110011 01100101 01110010 01110110 01101001 01100011 01100101 01110011
// strlen 7
00000111
// str _dns-sd
01011111 01100100 01101110 01110011 00101101 01110011 01100100
// strlen 4
00000100
// str _udp
01011111 01110101 01100100 01110000
// strlen 5
00000101
// str local
01101100 01101111 01100011 01100001 01101100
// strlen 0 (done)
00000000
// qtype 12 (PTR)
00000000 00001100
// qclass 1 (IN)
00000000 00000001
dig example
dig @224.0.0.251 -p 5353 -t ptr _services._dns-sd._udp.local
Note: dig
will exit after the first response, which will probably
be your local machine, so you'll have to write code to see all of them.
node.js example
A few mDNS libraries exist for node.js both that connect with the native mDNS Responder implementations on both OS X and Linux as well as pure javascript-only versions.
This sample code is provided for educational purposes of understanding how to implement mDNS. I don't recommend using this in production.
Sending a query
var DNSPacket = dnsjs.DNSPacket;
var DNSRecord = dnsjs.DNSPacket;
var packet = new DNSPacket();
packet.question.push(new DNSRecord(
'_services_dns-sd._udp.local' // Name
, DNSRecord.Type.PTR // Type 12
, DNSRecord.Class.IN // Class 1
//, null // optional TTL
));
socket.send(DNSPacket.toBuffer(packet), port, broadcast, function () {
console.log('sent query');
});
I'm here!
Generally speaking it's the sole job of a system's mDNS Responder to answer incoming mDNS queries to say I'm here!.
For educational purposes, this is what the I'm here! looks like for an Apple HomeKit device:
English
"Yes, I'm a device that handles the Apple HomeKit protocol!"
DNS Response
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: <<random>>
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;_services._dns-sd._udp.local. IN PTR
;; ANSWER SECTION:
_services._dns-sd._udp.local. 4500 IN PTR _homekit._tcp.local.
Hexidecimal
000084000000000100000000095f7365727669636573075f646e732d7364045f756470056c6f63616c00000c0001000011940010085f686f6d656b6974045f746370c023
// DNS Header
000084000000000100000000
// Name "9_services7_dns-sd4_tcp5local0"
09 5f7365727669636573 07 5f646e732d7364 04 5f756470 05 6c6f63616c 00
// Type, Class, TTL, RData Len
000c
0001
00001194
0010
// PTR "8_homekit4_tcp", compression pointer to "5local"
08 5f686f6d656b6974 04 5f746370
c0 23
Binary
//
// DNS Header
//
ID: 00000000 00000000
// codes and flags
RD: 0
TC: 0
AA: 1
OPCODE: 0000
QR: 1
RCODE: 0000
CD: 0
AD: 0
Z: 0
RA: 0
// counts
Q: 00000000 00000000
ANS: 00000000 00000001
AUTH: 00000000 00000000
ADD: 00000000 00000000
//
// Answer Section
//
// Name : 9_services7_dns-sd4_udp5local0 => _services._dns-sd._udp.local
// strlen 9
00001001
// str _services
01011111 01110011 01100101 01110010 01110110 01101001 01100011 01100101 01110011
// strlen 7
00000111
// str _dns-sd
01011111 01100100 01101110 01110011 00101101 01110011 01100100
// strlen 4
00000100
// str _udp
01011111 01110101 01100100 01110000
// strlen 5
00000101
// str local
01101100 01101111 01100011 01100001 01101100
// strlen 0 (done)
00000000
// Resource Record
// type 12 (PTR)
00000000 00001100
// class 1 (IN)
00000000 00000001
// ttl 4500 (int 4-bytes)
00000000 00000000 00010001 10010100
// rdata len 16
00000000 00010000
// strlen 8
00001000
// str "_homekit"
01011111 01101000 01101111 01101101 01100101 01101011 01101001 01110100
// strlen 4
00000100
// "_tcp"
01011111 01110100 01100011 01110000
// 0xc0 "compression pointer" 35 "5local"
// (the rest of the record is "in-bailiwick" and starts at the 36th byte of the full message)
11000000 00100011
node.js
Parsing answers in a response
socket.on('message', function (message, rinfo) {
console.log('Received %d bytes from %s:%d\n',
message.length, rinfo.address, rinfo.port);
console.log(message.toString('hex'));
var packets;
try {
packets = dnsjs.DNSPacket.parse(message);
}
catch (er) {
console.error(er);
return;
}
if (!Array.isArray(packets)) { packets = [packets]; }
console.log(packets);
console.log('\n');
});
Note: mDNS tries to be less chatty by using caching, so if you make a similar query twice
before the ttl is up the response will broadcast out to all of 224.0.0.251
the first time
and then only to the address that made the query the second time.
Hub by ppl
To find Hub on your network you skip the initial discovery process and straight to
querying for _hub._tcp.local
:
dig @224.0.0.251 -p 5353 -t PTR _hub._tcp.local
That will return not only an ANSWER section with a PTR for a specific cloud instance, but also an ADDITIONAL section with Hub's name, IP address, and version information.
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14657
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 6
;; QUESTION SECTION:
;_hub._tcp.local. IN PTR
;; ANSWER SECTION:
_hub._tcp.local. 10 IN PTR myrandomid._hub._tcp.local.
;; ADDITIONAL SECTION:
myrandomid._hub._tcp.local. 10 IN SRV 0 0 443 myrandomid.local.
myrandomid._hub._tcp.local. 10 IN TXT ""
myrandomid._device-info._tcp.local. 10 IN TXT "model=HubHome1,1" "pplvers=1"
myrandomid.local. 10 IN AAAA fe80::aabb:cfff:fe0f:f4a2
myrandomid.local. 10 IN A 10.0.0.33
myrandomid.local. 10 IN AAAA 2601:681:300:92c0:aabb:cfff:fe0f:f4a2
The setup app uses this information to connect establish a private connection to Hub by it's IP address and begin the setup process.
If you already knew the ip address of Hub you could look up its name using dig.
dig @224.0.0.251 -p 5353 -x 192.168.1.100
Or if debugging connected to a direct local ethernet connection (use case: raspberry pi)
on your laptop (not directly to your router) you could specify the interface as well with -b
:
dig @224.0.0.251 -p 5353 raspberrypi.local -b 192.168.2.3
It should also be discoverable with arp
arp -a
Resources
- http://www.binarytides.com/dns-query-code-in-c-with-linux-sockets/
- http://stackoverflow.com/questions/12681097/c-choose-interface-for-udp-multicast-socket
- https://github.com/mdns-js/node-dns-js/blob/master/lib/dnspacket.js
- https://developer.apple.com/library/content/qa/qa1337/_index.html
- Excellent breakdown of Answer, Authority, etc: http://www.zytrax.com/books/dns/ch15/
- http://docstore.mik.ua/orelly/networking_2ndEd/dns/ch15_02.htm
- Why mDNS discovery should be off by default https://mdns.shadowserver.org/
By AJ ONeal
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )