mDNS vs DNS: my personal take
· 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!