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 /etc/config/ohybridproxy:

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 lan.example.org into mDNS queries on the lan interface, and queries for wifi.example.org 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

The 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 services (see 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 2001:db8:1:: subnet):

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.1.0.0.0.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 nsregd server in that domain by issues an SRV query for _nsreg._tcp.example.org (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 configure).

In my case, I run nsregd on a separate server (nsregd.example.org in 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:
          - 1.0.0.0.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 10.0.0.1/27 and 2001:db8:1::/64 subnets to register new names, and will update 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 records.

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.

Closing remarks

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.