Naming and service discovery on a routed IPv6 network
I tend to eschew the normal practice of bridging WiFi and Ethernet networks in my private setups, instead running them on separate subnets and routing between them. This helps keep the amount of multicast traffic on the WiFi down, which is good. But it makes some things break, most notably DNS service discovery (AKA Zeroconf; this is the thing that makes printers and airplay devices pop up automatically).
Another issue I’ve been having is how to find my devices on an IPv6 network, where addresses are dynamic and devices pick them autonomously. In an IPv4 world, the DHCP server knows the addresses of all devices on the network (and Dnsmasq has excellent support for putting that into DNS), but this is not the case for IPv6. Dnsmasq does have support for guessing IPv6 addresses when handing out IPv4 addresses, but this doesn’t work if the client uses something other than SLAAC (such as RFC7217 addresses), and it won’t work in tomorrow’s IPv6-only world. Besides, I want the ability to put the global IPv6 addresses into public DNS, secured with DNSSEC.
In this post I’ll explain how I solve both of the above problems using the Unbound resolver running on my gateway router, the ohybridproxy mDNS proxy and Nsregd, a tool I wrote myself for registering addresses via signed DNS updates.
Making DNS service discovery work
DNS service discovery works by sending out Multicast DNS (mDNS) queries on the local link to discover services. Since the multicast discovery packets stay in the same layer 2 domain, when the WiFi network is not bridged to the LAN, a client on the WiFi can’t find a service on the LAN (and vice versa). So my roommate’s Apple laptop can’t find the printer that is connected to the wired LAN.
Fortunately, there exists a
specification for hybrid discovery
via unicast DNS queries, which we can use to re-enable service discovery
across the routed subnets. The
ohybridproxy implementation of
the proxy is packaged for OpenWrt and is straight forward to set up.
Simply install the
ohybridproxy package, and configure it in
config main main option host '::1' option port '5533' config interface option interface lan option domain lan.example.org config interface option interface wifi option domain wifi.example.org
This will cause the proxy server to listen for unicast DNS queries on
localhost port 5533 and translate queries for
mDNS queries on the
lan interface, and queries for
into mDNS queries on the
wifi interface. Make sure both the
ohybridproxy and the
mdnsd service are running.
The local unbound resolver can then be configured to forward queries for these domains to the hybrid proxy:
server: local-zone: "lan.example.org." nodefault local-zone: "wifi.example.org." nodefault local-zone: "example.org." typetransparent local-data: "b._dns-sd._udp.example.org. IN PTR lan.example.org." local-data: "b._dns-sd._udp.example.org. IN PTR wifi.example.org." local-data: "db._dns-sd._udp.example.org. IN PTR lan.example.org." local-data: "lb._dns-sd._udp.example.org. IN PTR lan.example.org." # These subnets are part of the guest network access-control-view: "10.1.1.64/27" "guest" access-control-view: "2001:db8:1::/64" "guest" view: # Disallow browsing for guests name: "guest" local-zone: "lan.example.org." refuse local-zone: "wifi.example.org." refuse stub-zone: name: "lan.example.org." stub-addr: ::1@5533 stub-zone: name: "wifi.example.org." stub-addr: ::1@5533
view part is for the case where a separate guest network is also
defined, which should not have access to the service discovery domains.
The local-data lines specify the PTR records that clients implementing
hybrid discovery will query for to discover in which domains to look for
section 5 of the draft
for details). The
example.org domain needs to be specified in the DHCP
config so clients receive it along with the DNS server to use.
And presto, laptops on the WiFi can print again!
Finding devices on the network
The other problem I was trying to solve was how to find devices on my
network. I.e., I want to be able to issue
ssh mymachine.example.org to
connect to a machine on my network, via IPv6, from anywhere. And I want
the address assignment to be secured with DNSSEC, but I don’t want to
manually maintain the zone file.
A mechanism for dynamically updating DNS zones has been around for 20 years in the form of DNS UPDATE queries. And signed updates are possible. However, using this means I would have to provision keys on all the devices that I want to register themselves, which is a pain.
To avoid this, I decided to write some code. What I came up with is Nsregd which is a DNS UPDATE proxy daemon that will allow clients on a network to issue signed updates for their hostname, authorising the updates on a Trust On First Use (TOFU) basis, similar to how SSH works the first time you connect to a host. What this means is that the first device to register a given name gets to “own” it, and subsequent updates for that names will only be allowed with the same key that initially registered it.
Nsregd will perform sanity checking on the registered names (only A and
AAAA records are allowed, and only for immediate subdomains of the
configured zone), then update the global DNS on the client’s behalf. It
can also optionally synthesise reverse PTR records for the names being
registered. Clients are expected to maintain their registration, and if
this is not done,
nsregd assumes the client has gone away and removes
the records from the DNS (but keeps the key so the client can re-claim
the name when it appears on the network again).
The repository linked above also contains a client (
nsregc) which will
try to discover where to find an
nsregd server for the current network
and register any addresses on the local machine with it, and maintain
the registration. Discovery is done by leveraging the same PTR magic
that the hybrid service discovery described above uses. On the unbound
server, I simply add the following config (for the
server: local-data: "r._dns-sd._udp.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.22.214.171.124.8.b.d.0.1.0.0.2.ip6.arpa. IN PTR example.org"
This specifies that a client on that subnet can register itself in the
example.org zone. The client will then try to find an
in that domain by issues an SRV query for
(which must be added to global DNS). If this returns a record, the
client will attempt registration with that server (which can be located
anywhere, as long as it is reachable from the subnet we want to
In my case, I run
nsregd on a separate server (
this example), which is configured like this:
listen-addr: nsregd.example.org listen-port: 5333 data-dir: /var/lib/nsregd zones: example.org: reserved-names: - localhost - guardian - ns allowed-nets: - 10.0.0.1/27 - 2001:db8:1::/64 max-key-ttl: 4320h max-addr-ttl: 1h allow-any-addr: false upstreams: - type: nsupdate hostname: ns.example.org port: 53 tcp: true timeout: 1s zone: example.org reverse-zones: - 126.96.36.199.8.b.d.0.1.0.0.2.ip6.arpa record-ttl: 60s keep-records: false tsig-name: nsregd.example.org. tsig-secret: "verysecretpassword" exclude-nets: - 10.0.0.0/8 - fd00::/8 - type: unbound hostname: guardian.example.org port: 8953 client-cert: /etc/nsregd/unbound-example_client.crt client-key: /etc/nsregd/unbound-example_client.key server-cert: /etc/nsregd/unbound-example_server.crt server-name: unbound zone-type: typetransparent timeout: 1s record-ttl: 60s keep-records: false reverse-zones: - 0.0.10.in-addr.arpa. exclude-nets: - 2000::/4 - fd00::/8
This config specifies that
nsregd will allow devices in the
2001:db8:1::/64 subnets to register new names, and
ns.example.org using TSIG-signed DNS updates, and an
unbound instance on
guardian.example.org (the gateway device). Only
globally routable addresses will be inserted into the global DNS, and
only local addresses will be added to unbound, and reverse records will
be created corresponding to each forward record the client adds. The
upstream DNS server is configured to automatically sign records added
via DNS UPDATE, so the global DNS view will have correctly signed
With this setup, all I need to do is run the
nsregc client on every
device I want to be able to find. The client will auto-discover the
configuration on the network and register the device; and keep that
registration up-to-date with changing IP addresses. This works quite
well; I am currently using it for three different networks, using the
same Nsregd instance.
As outlined above, hybrid DNS-SD discovery can be used to make Zeroconf discovery work on a routed network, and is quite straight-forward to setup on an OpenWrt router. And using the Nsregd daemon and its corresponding client, devices can register their own names on the network, and the registration will go into public DNS, making it possible to find the device from anywhere.
I first discovered both of these mechanisms in the IETF homenet working group, and Nsregd is inspired by the naming architecture document currently being fleshed out in the group.