mDNS vs DNS: my personal take

mDNS vs DNS: my personal take

By Marco Antonio Garrido
· 1,340 words · 7 minutes reading time

Like many other software enthusiasts, I enjoy self-hosting some services at home. Nothing crazy. For example, I just like having my private repositories stored in an instance of Gitea, or accessing my film collection through a Jellyfin server. And again, like many other software enthusiasts, I end up typing the IP and the port where those services provide their web applications every time I use them.

There is nothing bad about that approach in a home network, but my non-techy wife doesn't share the same opinion. I know I can do better and just for her I'll set up some kind of local domain name resolution so she can stop thinking that I'm a savage.

mDNS: the zero-configuration approach

Computers (i.e. anything with a CPU and some memory) have been at home for some decades now. Which means that they are tame plug-and-play machines thanks to the effort of many engineers. Today I feel thankful to the people behind mDNS.

The multicast DNS protocol pretty much resolves the hostnames of the devices in a local network into their IPs. That way, you can find any device advertising their hostname at <hostname>.local.

The best thing about mDNS is that it usually comes pre-installed in your favourite operating system, in my particular case Raspbian, as my humble home server is a Raspberry Pi. I didn't have to do anything, but otherwise, I would just have to install Avahi, the most common implementation for Linux.

So where is the problem? First, even with the hostname resolution, I still need to provide the port number in the URL. Fair, that's not a problem with mDNS, I just need to host the webapps in port 80 (or 443 if I configure a certificate). But still, I can only host a single application in those ports. While I can come up with a clever solution with Nginx as a reverse proxy, I still would like to have multiple domains, one for each one of the services that I host.

While mDNS can be configured for advertising multiple names, there is a bigger problem: Android doesn't fully support mDNS. As far as I could check, there is some support in their SDKs, but not all apps make use of it and, most importantly, Chrome doesn't resolve the local domains at all. At least, that was my experience with the phones at home.

At this point, I don't have good reasons for using mDNS at home, which is a pity because, if my wife and I had been part of the iPhone gang, this would have worked.

My dearest old friend, DNS

Things can't go wrong with DNS. I just need to create a DNS server and I can do that with BIND. BIND is the most common solution for DNS servers, currently maintained by the Internet Systems Consortium (ISC). The installation instructions are clear and they even offer a Docker image.

Unfortunately, the image doesn't work for the Raspberry Pi, however, ISC has published the Dockerfile in a repo so it can be built for your favourite architecture. Instead, I'll customize it and use my own simplified Dockerfile:

FROM alpine:latest

COPY init.sh /init.sh
RUN chown root /init.sh && chmod 755 /init.sh

RUN apk --update add bind

RUN mkdir -p /etc/bind && chown root:named /etc/bind/ && chmod 755 /etc/bind
RUN mkdir -p /default && cp -R /etc/bind /default

EXPOSE 53/udp 53/tcp

VOLUME ["/etc/bind"]

CMD /init.sh

The main differences are that I use Alpine instead of Ubuntu and I only care about the content of /etc/bind, the directory with the configuration files, so I define a single volume for persisting them (I don't need the logs nor anything else for this setup).

Additionally, I decided to copy the content of that directory into /default so I can copy the content back to /etc/bind if the host directory for the volume is empty. I did that just because I preferred to modify the default config files from the host while I was playing around with BIND. That is done by the script in init.sh, which also takes care of starting BIND (or named, its server daemon):

if [ -z "$(ls -A "/etc/bind")" ]; then
  cp -R /default/bind/* /etc/bind/
fi

/usr/sbin/named -f -c /etc/bind/named.conf -u named

The last piece regarding the Docker configuration is the compose.yaml file:

version: "3"

services:
  bind:
    container_name: bind
    build:
      context: .
      dockerfile: Dockerfile
    image: custom-bind:1.0
    ports:
      - "53:53/tcp"
      - "53:53/udp"
    volumes:
      - './etc-bind:/etc/bind'
    restart: always

Let's talk now about the config files for BIND, which will be stored locally in ./etc-bind. The main configuration file is named.conf, which has the following content:

options {
	directory "/var/bind";

	allow-recursion {
		0.0.0.0/0;
	};

	forwarders {
		1.1.1.1;
		1.0.0.1;
	};

	pid-file "/var/run/named/named.pid";

	allow-transfer { none; };
};

zone "local" {
	type master;
	file "/etc/bind/db.local";
};

What I'm doing here is defining a zone for .local domains, which will be configured in the db.local file. For any request about any other domain, I'll forward the request to 1.1.1.1 or 1.0.0.1 (Cloudflare's DNS servers). At last, I define the DNS records for the zone in db.local:

$TTL    604800
@         IN      SOA     ns.local. admin.local. (
                    2023122900 ; Serial
                    604800     ; Refresh
                    86400      ; Retry
                    2419200    ; Expire
                    604800 )   ; Negative Cache TTL

@         IN      NS      ns.local.

ns        IN      A       <DNS-server-IP>
server1   IN      A       <server-1-IP>
server2   IN      A       <server-2-IP>

With this configuration, I can go ahead and use server1.local or server2.local instead of the IPs for those servers. This is it, I just need to start the Docker container with docker compose up --build -d.

Is this an orthodox approach?

Nope. I made some personal decisions here and some people might consider them wrong.

First, it is discouraged to combine an authoritative DNS server with a recursive DNS configuration. And that's what I'm doing here, but I don't feel like setting up two separate servers for my home network.

Secondly and more importantly, I keep using .local domains outside the mDNS context. The main reason is because I couldn't think of a better domain name. There are not many reserved domains and none fits the purpose of my setup. .home would have been a good option, as it was rejected as a generic top-level domain, but Firefox doesn't recognize it either and it considers it a search query, unless I prepend the protocol (e.g. http://) in the search bar. .home.arpa looks like a safer option as it is proposed by RFC 8375, but still .local is simpler.

I'll stick to this approach for now until I'm convinced by someone with stronger arguments than mine. Therefore, I have to pay a toll for my decision and change the configuration of systemd in my Fedora workstation so .local domains aren't resolved through mDNS. I changed the content of /etc/systemd/resolved.conf with the following configuration:

[Resolve]
DNS=<DNS-server-IP>
Domains=~.
MulticastDNS=false
LLMNR=false

I'm just missing to execute sudo systemctl restart systemd-resolved. For other devices, like the smartphones at home, I simply update the configuration in my router so it starts using my DNS server and I'm done!

Conclusion

I would have preferred to leverage mDNS for the name resolution in my home network but I had to end up using old good DNS and setting up a BIND server. I enjoyed the journey and I'm happy with the final solution. Now I can go ahead and create records for my self-hosted services and, either, use a reverse proxy in front of them, or assign different IPs to each one of them.

All the configuration files that I used in this post can be found in this repo. Have fun with them!


About the author

Marco Antonio Garrido

Freelance Software Engineer

I'm a Full-Stack Software Engineer with more than 5 years of experience in the industry. After working 4 years as a SDE at Amazon, I decided to start a new chapter in my career and become a freelancer.

LinkedIn | GitHub