Linux Fu: The Local Phonebook

I’ll admit it: I miss the simplicity of /etc/hosts. There was something elegant about it. You wanted laserprinter to mean 192.168.1.40, so you opened a text file and wrote:

192.168.1.40 laserprinter

Done. No cloud account, no discovery daemon, no dashboard with material-themed icons. Just a name and an address. The trouble, of course, is that /etc/hosts is only simple when you have one machine. The moment you have a desktop, a laptop, a Raspberry Pi, a NAS, a test box, and a phone or two, every little network change becomes a tiny distributed-database problem. Which copy of /etc/hosts is authoritative? Did you update the laptop? What about the machine you only boot once a month?

One Solution

Modern LANs solved this with mDNS, using Avahi on Linux. It resolves addresses that end in .local. Instead of asking a central DNS server “who is thing.local?”, a machine sends a multicast query on the local network: “who has thing.local?” The device that owns the name answers. This is why your Linux box named spock and usually be reached as spock.local on your LAN.

There are limits. mDNS is link-local; it is meant for the local LAN, not the whole Internet and shouldn’t route across subnets. Each device is supposed to publish its own name. That works fine when the device cooperates. But what about devices that do not publish mDNS? Or little embedded things that barely even have an IP address?

That is where I wanted the best of both worlds: keep a small authoritative /etc/hosts file on one Linux box, but publish selected entries onto the LAN using mDNS.

Publishing House

Avahi includes a handy tool for this:

avahi-publish-address widget.local 192.168.1.50

While that process is running, Avahi advertises widget.local as an mDNS address record. Kill the process, and the record goes away. So you could just write a script to publish all the addresses for things that won’t do it themselves and launch in in local.rc or a systemd unit. But that seems inelegant. I wanted to just pick things out of the /etc/hosts file. But not everything. Here is a simple publisher, installed as /usr/local/sbin/localip_pub:

#!/bin/sh
# Scan /etc/hosts for .local addressess and publish them using avahi

tmp="$(mktemp)"
pids=""

cleanup() {
   rm -f "$tmp"
   for p in $pids; do
      kill "$p" 2>/dev/null
   done
   wait
}


mdns_name_exists() {
   timeout 2 avahi-resolve-host-name -4 "$1" 2>/dev/null |
      awk -v h="$1" '
         BEGIN {
            sub(/\.$/, "", h)
            rc = 1
         }
      {
         n = $1
         sub(/\.$/, "", n)
      }
      n == h && $2 ~ /^([0-9]{1,3}\.){3}[0-9]{1,3}$/ {
      rc = 0
   }
   END {
      exit rc
   }'
}

trap cleanup INT TERM EXIT

awk '
   NF < 2 { next } # skip short lines
   $1 ~ /^127\./ { next } # skip local address
   # This assumes  .local
   # it will reject anything with an alias or subdomain or trailing comment
   # it also rejects any full comment lines or IPv6 addresses
   # although you could certainly patch it for IPv6 if you wanted to
   /^[[:space:]]*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]]+[^.]+\.local[[:space:]]*$/ {
      ip=$1
      name=$2
      if (!seen[ip]++) print name, ip
   }
' /etc/hosts > "$tmp"


while read name ip; do
   if mdns_name_exists "$name"
   then
      echo found $name
      continue
   fi
echo avahi-publish-address "$name" "$ip"
avahi-publish-address "$name" "$ip" &
pids="$pids $!"
done < "$tmp"
wait

The script is intentionally conservative. It ignores loopback, ignores IPv6, and only publishes single names that already end in .local. So consider this hosts snippet:

192.168.1.51 oscope.local
192.168.1.52 server.example.com

The script will publish oscope.local, but leaves server.example.com alone. That avoids accidentally dumping every fully qualified name in your hosts file into mDNS.

But Wait…

The wait at the end of the script matters. Each avahi-publish-address process has to stay alive for the record to remain published. If the shell script simply started them in the background and exited, systemd would consider the service finished and might clean up the child processes. By waiting, the script remains the service’s main process.

Here is the matching systemd unit:


<h1>/etc/systemd/system/localip_pub.service</h1>

[Unit]
Description=Publish selected /etc/hosts names via Avahi/mDNS
Requires=avahi-daemon.service
After=network-online.target avahi-daemon.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/sbin/localip_pub
Restart=on-failure
RestartSec=5s
KillMode=control-group

[Install]
WantedBy=multi-user.target

Install and start it:


sudo chmod 755 /usr/local/sbin/localip_pub
sudo systemctl daemon-reload
sudo systemctl enable --now localip_pub.service

After editing /etc/hosts, restart the publisher:

sudo systemctl restart localip_pub.service

Of course, if you want to use this, it has to be a machine that knows to query mDNS. Ironically, your workstation probably does, but if it is configured to use /etc/hosts first, that won’t matter there. But it does lead to a practical annoyance: not every application on every platform uses mDNS the same way. Android itself only relatively recently grew better support for normal application-level mDNS resolution, so behavior can vary depending on Android version, app, and resolver path. Chrome on Android seems to resolve .local names correctly in my testing.

Firefox was a little more subtle. At first it looked like Firefox on Android simply did not understand mDNS, but the real culprit was Secure DNS. If Firefox is configured to use DNS-over-HTTPS, it may bypass the local resolver path that knows how to handle .local names. Turning Secure DNS off, or adding exceptions for the local names you care about, lets those names resolve normally. That is not really an Avahi problem; it is an application side effect. To be fair, even if you stood up a LAN DNS server to solve the problem, Firefox would have probably still have skipped it in favor of its “secure” DNS.

Conflicting Data

There is another gotcha: name conflicts are not the only kind of conflict. You might expect trouble if you try to publish widget.local when some other device already owns widget.local, but address conflicts can be troublesome too. If some device is already publishing mDNS information associated with a particular IP address, trying to publish a second name for that same address may fail. In my case, avahi-publish-address did not fail gracefully; it wandered into a collision path and crashed. The safe approach is to check before publishing, avoid duplicate names and duplicate addresses, and let the device itself publish its own mDNS name when possible.

Still, for a small LAN, this is a nice compromise. /etc/hosts remains the simple text file I wanted, but Avahi turns selected entries into something other machines can discover without copying that file everywhere. It is not a replacement for real DNS, and it is not quite as seamless as the old single-machine hosts file. But for those odd devices that sit quietly on the network with an IP address and no useful name, it is a handy little bridge between the old world and the new one. Were there other ways to solve this? There always are. But this works for me.

We’ve looked under the covers at mDNS. The system can be a lifesaver when you have too many Raspberry Pis.

30 thoughts on “Linux Fu: The Local Phonebook

  1. I need to sit down and learn more about mdns, Avahi and .local addresses. But… I’m not sure they are always the best solution. I mean sure.. for the printer example they are perfect. But… if you have services that you port forward so they can be accessed from the internet.. and you have portable devices that you access them from both on and off the LAN I’m not sure that’s a perfect solution.

    I like just setting up DNS overrides in my router. Each device on my LAN which has internet-exposed services gets it’s own subdomain. On the LAN I access the DNS server running in my router and it returns the respective local IPs. But from the internet I just have a wildcard subdomain mapped back to my home IP.

    This way for my portable devices.. it doesn’t matter if I am home on the LAN, away using the internet or even away and VPN’ed into my home network. I only need one stored url for each service and everything just works.

        1. That first line started with one octothorpe and a space.
          The second line two octothorpes and a space.

          Well that’s an annoying worst-of-all-worlds feature.

          The octothorpes are filtered out so you can’t even start a line with that character.

          I can see not wanting to let people make their comments all huge and annoying with formatting but silently dropping characters? That kind of sucks.

  2. I can understand the desire to have some sort of EZ-autodiscover mechanism, either for home users who really don’t want to touch even the most rudimentary DNS config or for devices that have essentially no provisioning interface and you’d otherwise need to hunt down more aggressively on the network; but I can never really get over how…unsafe…it feels to have a “Everyone just picks a name and announces it while everyone else blindly treats that as acceptable” network addressing mechanism.

    If I’m on a network where I trust everyone enough for that to be cool I probably don’t need an additional way of knowing who people are; and if I’m not on such a network then no, I don’t really take your word for it.

    Obviously turning it into some ‘trusted client’ hell where only mDNS records signed with TPM keys from blessed vendors is exceptionally the wrong approach; but you can’t ignore the fact that (while not necessarily available to non-root users depending on an OS’ local setup) mDNS advertisements are basically held together by good faith and wishes.

    This will absolutely 100% never happen; but I’d love for there to be some (probably of most interest to nerds; but consumer-accessible) keyfill/provisioning mechanism. It would be a cold day in hell before we see U-229 key fill connectors; but there has to be some sort of USB port or contactless NFC option that would allow you to perform some of those analogous functions in a more consumer electronics friendly package; with modes of operation ranging from “assume your router is in physically secure location; bump new device up against it to get it online and get it a DNS entry” to fancier setups where a device is having some config and an x.509 for 802.1X slapped into it.

  3. Why would you do this with avahi when dnsmasq is right there looking at you? Install it, point it at your upstream DNS server and add some host records. Tell your DHCP server to provide that machine’s IP address as the DNS server.

    1. Well I prefer not to have my local machine be the DNS for the entire LAN. I typically have DHCP hand out an external DNS server (and I control my domains remotely — see the Linux Fu about how I do dynamic DNS for example where I control named remotely for that purpose). So, yah, like I mentioned there are lots of ways to do this. Running DNS somewhere on the LAN and managing another DNS server is an option. But just editing my /etc/hosts file and automatically exposing the name to everyone on the LAN regardless of their DNS settings is very attractive. Not the only way and your needs may be different than mine. Still nice to understand how mDNS works.

      1. It’s nice to understand how it works and all, but in my mind DNS should be one of the core functions of your network hardware, not an entirely separate server to manage. Maybe customer routers don’t offer the ability, but there’s a lot of customer routers that are useless at all sorts of things so that doesn’t mean much to me.

        I much prefer being able to make a dhcp reservation, create an internal domain name for that device, and define rules on who locally or on wan can access that device and how, all in the same pane of glass. Also lets you subscribe to DNS filter lists, translate insecure to secure DNS before it reaches the internet, or even do different things for different clients, all without really impacting performance, power consumption, or management work, and without having to tolerate rogue conflicting behavior from lan devices making everything much more annoying to admin.

        You bought a new security camera that you want to access at camera7.internal but only by certain devices on certain interfaces, and you want it prohibited from phoning home to the cloud? Easy, once you make the dhcp reservation just create a new dns record for camera1.internal pointing to the same ip address object and then add that object to your camera access rule, likely by putting the address in a camera address group. If you ever need to change it, you change it once for everything. If you want a better name? CNAME it so you can also call it cam7.internal, chickencoop.internal, or whatever you like.

    2. huh. i personally use bind for this…i already was running bind locally for other reasons before i decided to move my /etc/hosts into it. and the one thing i don’t like about bind is that i have to add each new device to two separate files for forward and reverse resolution. i guess dnsmasq just uses /etc/hosts. so if it ever breaks and i have to rebuild it, i guess i will look into using dnsmasq, which is probably a better fit for my needs anyways. thanks!

  4. there’s a lot i don’t like about this approach but i appreciate the article’s description of mdns (it is exactly as i had imagined it). but the funniest thing for me is how avahi-publish-address doesn’t need root (which is a big problem with mdns in my mind), but in order to ask systemd to let you run a non-interactive process ‘the right way’, you do need root. man i hate systemd :)

    1. Not a huge fan of systemd either, but you always needed root access to create a non-interactive startup service; you can’t edit the necessary sysvinit scripts without it.

      there were still sometimes sneaky ways to run something non-interactive at boot without root access, such as if your system cron supports the @reboot tag for non-root crontabs.

      (incidentally, systemd allows users to create and manage user-level services without root, but it doesn’t help in this situation since they only start up when the user logs in)

  5. To be fair, even if you stood up a LAN DNS server to solve the problem, Firefox would have probably still have skipped it in favor of its “secure” DNS.

    In my testing, this works fine- when Android Firefox doesn’t get a hit on the “secure” DNS, it queries the ordinary DNS and picks up any LAN addresses that are there. But it still won’t resolve mDNS “.local” addresses for some reason unless you disable it entirely.

    This is a specific “Firefox for Android” quirk; desktop Firefox does resolve .local addresses in this scenario (as long as the system DNS resolver supports them).

    1. What does android firefox do if secure DNS doesn’t just lack a record but actually isn’t reachable? I don’t use mdns like this, so I haven’t noticed what happens, but then my picture of reasonable things to do with your home network involves blocking all DNS that isn’t aimed at your own preferred server.

  6. The ONLY reasons why I edited hosts file for the last 20 years was to:

    - prevent Adobe CS6 from phoning home and getting de-activated

    - prevent Typora beta from phoning home and showing “this version of Typora is expired kthxbai”

    - prevent myself from visiting (even accidentally) 60 most popular sites in my country which by early 2010s went from showing news or other useful information to delivering clickbait content

  7. The first thing I do when I set up a linux install is disable things. One of them is avahi. I believe it’s useful but I think it’s crazy for it to be enabled by default. This is half because it enlarges the attack surface of a networked computer and half because I have an irrational distrust of the guy who made it. I do have fond memories of realizing I could type “ssh [user]@[hostname].local” instead of opening the router config to check what IP a device had. I suppose I wish I could choose to whitelist devices or hostnames. I’ve never used more than two named devices so it feels wasteful/reckless to have a service always listening for any device.

  8. My router runs openWRT and provides dnsmasq which is nicely integrated into the DHCP server as well. So if a device connects with a client name, I can go to [clientname].lan, and if I want to assign it a name I can give it a DHCP static lease and as many hostnames as I want, which is useful for name-based vhosting on my various homelab servers as well. This is way more robust and less fragile than /etc/hosts, and also allows it to work from my devices which don’t support /etc/hosts overrides (such as my phone and tablet).

    I don’t see the rationale behind doing this via mDNS unless you really want to use .local addresses, but in my experience those are also pretty fragile since the client-side resolver can screw up in all sorts of fascinating ways.

  9. The cleanup having
    rm -f “$tmp”
    without any checks on tmp makes me nervous. What are the odds that I use tmp at a later time? Can mktemp fail when not in path? Maybe add a set -e ?

  10. There’s A Better Way To Do This™.

    Originally my objection was that having .local addresses in /etc/hosts would mean that your server isn’t dogfooding itself — because those names are in its hosts file, it won’t need to look them up using mDNS. It’d be preferable to have the names configured somewhere else, and not in the hosts file, so that when you ping mydevice.local even from the server you know that it’s using Avahi to look up the name.

    But then I realized, if publishing static entries in Avahi is a useful thing, surely they’ve provided a mechanism for that. And sure enough, the avahi.service man page details the means by which static service definitions can be dropped into /etc/avahi/services/ as foo.service files, defining the static entries it should publish. No /etc/hosts editing needed, and no “one avahi-publish-address process running per service published” wastefulness, because it’s all handled directly within the avahi-daemon process. (Which it is even using -publish-address, that’s just a tool that connects to avahi-daemon and gives it the information to publish. But statically configuring that in /etc/avahi/services/ is a much better way to go.

    1. avahi.service files are XML, which is “meh”, but there’s at least a DTD provided in /usr/share/avahi/avahi-service.dtd so that you can verify your service definitions using standard XML tools. There are probably tools (traditional or agentic) that can use it to help with creating one, too.

  11. .local? A proper DNS is the correct solution, and .internal or .home.arpa is the correct. Negates network conflicts, SSL cert errors, and slow name resolution. Run avahi if you have a compelling reason to, and note that .local will break Pi-hole. But it’s your LAN it how you like.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.