Linux Fu: The Local Phonebook

Diablo

New member
LinuxFu.jpg


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.
 
Back
Top