View Source MdnsLite

Hex version CircleCI

MdnsLite is a simple, limited, no frills implementation of an mDNS (Multicast Domain Name System) client and server. It operates like DNS, but uses multicast instead of unicast so that any computer on a LAN can help resolve names. In particular, it resolves hostnames that end in .local and provides a way to advertise and discovery service.

MdnsLite is intended for environments like on Nerves devices that do not already have an mDNS service. If you're running on desktop Linux or on MacOS, you already have mDNS support and do not need MdnsLite.

Features of MdnsLite:

  • Advertise <hostname>.local and aliases for ease of finding devices
  • Static (application config) and dynamic service registration
  • Support for multi-homed devices. For example, mDNS responses sent on a network interface have the expected IP addresses for that interface.
  • DNS bridging so that Erlang's built-in DNS resolver can look up .local names via mDNS.
  • Caching of results and advertisements seen on the network
  • Integration with VintageNet and Erlang's :inet application for network interface monitoring
  • Easy inspection of mDNS record tables to help debug service discovery issues

MdnsLite is included in NervesPack so you might already have it!

Configuration

A typical configuration in the config.exs file looks like:

config :mdns_lite,
  # Advertise `hostname.local` on the LAN
  hosts: [:hostname],
  # If instance_name is not defined it defaults to the first hostname
  instance_name: "Awesome Device",
  services: [
    # Advertise an HTTP server running on port 80
    %{
      id: :web_service,
      protocol: "http",
      transport: "tcp",
      port: 80,
    },
    # Advertise an SSH daemon on port 22
    %{
      id: :ssh_daemon,
      protocol: "ssh",
      transport: "tcp",
      port: 22,
    }
  ]

The services section lists the services that the host offers, such as providing an HTTP server. Specifying a protocol, transport and port is usually the easiest way. The protocol and transport get combined to form the service type that's actually advertised on the network. For example, a "tcp" transport and "ssh" protocol will end up as "_ssh._tcp" in the advertisement. If you need something custom, specify :type directly. Optional fields include :id, :weight, :priority, :instance_name and :txt_payload. An :id is needed to remove the service advertisement at runtime. If not specified, :instance_name is inherited from the top-level config. A :txt_payload is a list of "<key>=<value>" string that will be advertised in a TXT DNS record corresponding to the service.

See MdnsLite.Options for information about all application environment options.

It's possible to change the advertised hostnames, instance names and services at runtime. For example, to change the list of advertised hostnames, run:

iex> MdnsLite.set_hosts([:hostname, "nerves"])
:ok

To change the advertised instance name:

iex> MdnsLite.set_instance_name("My Other Awesome Device")
:ok

Here's how to add and remove a service at runtime:

iex> MdnsLite.add_mdns_service(%{
    id: :my_web_server,
    protocol: "http",
    transport: "tcp",
    port: 80,
  })
:ok
iex> MdnsLite.remove_mdns_service(:my_web_server)
:ok

Client

MdnsLite.gethostbyname/1 uses mDNS to resolve hostnames. Here's an example:

iex> MdnsLite.gethostbyname("my-laptop.local")
{:ok, {172, 31, 112, 98}}

If you just want mDNS to "just work" with Erlang, you'll need to enable MdnsLite's DNS Bridge feature and configure Erlang's DNS resolver to use it. See the DNS Bridge section for details.

Service discovery docs TBD...

DNS Bridge configuration

MdnsLite can start a DNS server to respond to .local queries. This enables code that has no knowledge of mDNS to resolve mDNS queries. For example, Erlang/OTP's built-in DNS resolver doesn't know about mDNS. It's used to resolve hosts for Erlang distribution and pretty much any code using :gen_tcp and :gen_udp. MdnsLite's DNS bridge feature makes .local hostname lookups work for all of this. No code modifications required.

Note that this feature is useful on Nerves devices. Erlang/OTP can use the system name resolver on desktop Linux and MacOS. The system name resolver should already be hooked up to an mDNS resolver there.

To set this up, you'll need to enable the DNS bridge on MdnsLite and then set up the DNS resolver to use it first. Here are the options for the application environment:

config :mdns_lite,
  dns_bridge_enabled: true,
  dns_bridge_ip: {127, 0, 0, 53},
  dns_bridge_port: 53

config :vintage_net,
  additional_name_servers: [{127, 0, 0, 53}]

The choice of running the DNS bridge on 127.0.0.53:53 is mostly arbitrary. This is the default.

Info

If you're using a version of Erlang/OTP before 24.1, you'll be affected by OTP #5092. The workaround is to add the dns_bridge_recursive: true option to the :mdns_lite config.

Debugging

MdnsLite maintains a table of records that it advertises and a cache per network interface. The table of records that it advertises is based solely off its configuration. Review it by running:

iex> MdnsLite.Info.dump_records
<interface_ipv4>.in-addr.arpa: type PTR, class IN, ttl 120, nerves-2e6d.local
<interface_ipv6>.ip6.arpa: type PTR, class IN, ttl 120, nerves-2e6d.local
_epmd._tcp.local: type PTR, class IN, ttl 120, nerves-2e6d._epmd._tcp.local
_services._dns-sd._udp.local: type PTR, class IN, ttl 120, _epmd._tcp.local
_services._dns-sd._udp.local: type PTR, class IN, ttl 120, _sftp-ssh._tcp.local
_services._dns-sd._udp.local: type PTR, class IN, ttl 120, _ssh._tcp.local
_sftp-ssh._tcp.local: type PTR, class IN, ttl 120, nerves-2e6d._sftp-ssh._tcp.local
_ssh._tcp.local: type PTR, class IN, ttl 120, nerves-2e6d._ssh._tcp.local
nerves-2e6d._epmd._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 4369, nerves-2e6d.local.
nerves-2e6d._epmd._tcp.local: type TXT, class IN, ttl 120,
nerves-2e6d._sftp-ssh._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 22, nerves-2e6d.local.
nerves-2e6d._sftp-ssh._tcp.local: type TXT, class IN, ttl 120,
nerves-2e6d._ssh._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 22, nerves-2e6d.local.
nerves-2e6d._ssh._tcp.local: type TXT, class IN, ttl 120,
nerves-2e6d.local: type A, class IN, ttl 120, addr <interface_ipv4>
nerves-2e6d.local: type AAAA, class IN, ttl 120, addr <interface_ipv6>

Note that some addresses have not been filled in. They depend on which network interface receives the query. The idea is that if a computer is looking for you on the Ethernet interface, you should give records with that Ethernet's interface rather than, say, the IP address of the WiFi interface.

MdnsLite's cache is filled with records that it sees advertised. It's basically the same, but can be quite large depending on the mDNS activity on a link. It looks like this:

iex> MdnsLite.Info.dump_caches
Responder: 172.31.112.97
  ...
Responder: 192.168.1.58
  ...

In memory

Peter Marks wrote and maintained the original version of mdns_lite.

License

Copyright (C) 2019-21 SmartRent

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.