Although 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


By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )