Let’s say that you need to resolve .local
addresses from either another subnet or a VPN of some sort. Since mDNS uses multicast on a local LAN segment, that won’t cross subnet boundaries and is generally impossible without workarounds most people don’t want to deal with (including me).
There are many ways to do this, but I’ve found that the simplest is to use plain DNS and set up a dedicated dnsmasq
to resolve .local
via standard DNS.
Tools like dig
will complain a bit, but it works well enough for my needs since you can just point any resolvers that need this to a dedicated DNS server–and standard DNS will be routed through your router or VPN gateway, so it will work just fine.
First of all (assuming Debian), you need to install these packages:
apt install avahi-daemon avahi-utils dnsmasq
And then set up a script to do the resolution inside the target subnet–I tried many approaches, but avahi-browse
consistently failed to pick up on some of my machines and LXC containers without rhyme nor reason, so I decided to add a little list of machines I wanted it to forcefully resolve every time:
#!/usr/bin/env python3
# /usr/local/bin/update_mdns_dnsmasq.py
import subprocess
import re
import os
import socket
from time import sleep
# make sure you uncomment the use of /etc/dnsmasq.d in dnsmasq.conf
DNSMASQ_CONF_PATH = "/etc/dnsmasq.d/mdns.conf"
# kinda obvious, I know
DNSMASQ_SERVICE = "dnsmasq"
# make sure we resolve these hostnames in case they're not being announced often enough
FORCE_RESOLVE = ["home", "borg", "diskstation", "platinum"]
def read_existing_mdns_entries(filepath):
"""Grab existing entries"""
entries = set()
if not os.path.exists(filepath):
return entries
with open(filepath, "r") as f:
for line in f:
if line.strip() and not line.startswith("#"):
parts = line.strip().split("/")
if len(parts) == 2:
hostname, ip = parts
entries[hostname.strip()] = ip.strip()
return entries
def discover_mdns_hosts(timeout=30):
"""Run avahi-browse with a sizable timeout and parse verbose output."""
try:
output = subprocess.run(
["/usr/bin/timeout", f"{timeout}", "avahi-browse", "-a", "-r"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
except subprocess.CalledProcessError as e:
return []
hosts = set()
current_hostname = None
current_address = None
for line in output.stdout.splitlines():
line = line.strip()
if "hostname" in line:
match = re.search(r"hostname = \[(.+?)\]", line)
if match:
current_hostname = match.group(1)
elif "address" in line:
match = re.search(r"address = \[(.+?)\]", line)
if match:
current_address = match.group(1)
# When both are captured, store and reset
if current_hostname and current_address:
if current_hostname.endswith(".local") and re.match(r"\d+\.\d+\.\d+\.\d+", current_address):
hosts.add((current_hostname, current_address))
current_hostname = None
current_address = None
for host in FORCE_RESOLVE:
name = f"{host}.local"
try:
ip = socket.gethostbyname(name)
hosts.add((name, ip))
except:
pass
return sorted(hosts)
def write_dnsmasq_config(hosts):
"""Write out a nice configuration file"""
lines = [f"address=/{hostname}/{ip}" for hostname, ip in hosts]
config = "\n".join(lines) + "\n"
with open(DNSMASQ_CONF_PATH, "w") as f:
f.write(config)
def restart_dnsmasq():
"""Reload doesn't actually force file reads, so we need to be more assertive here"""
subprocess.run(["systemctl", "restart", DNSMASQ_SERVICE], check=False)
def main():
while True:
existing = read_existing_mdns_entries(DNSMASQ_CONF_PATH)
existing.update(discover_mdns_hosts(30))
write_dnsmasq_config(sorted(existing))
restart_dnsmasq()
sleep(30)
if __name__ == "__main__":
main()
This isn’t overly clean (it doesn’t know anything about IPv6 and will let dead entries linger forever), but it works well enough for me, and adding a separate state file with “last seen” timestamps and clearing out old entries after a day or so is an exercise I leave to my readers.
I saved the script to /usr/local/bin/update_mdns_dnsmasq.py
and wrote a simple systemd
unit file for it:
# /etc/systemd/system/mdns-sync.service
[Unit]
Description=Sync mDNS to dnsmasq
After=network.target
[Service]
ExecStart=/usr/local/bin/update_mdns_dnsmasq.py
Restart=always
[Install]
WantedBy=multi-user.target
One thing that you absolutely must do after enabling dnsmasq
as well is to check /etc/dnsmasq.conf
and ensure it’s reading /etc/dnsmasq.d
, since the defaults may vary by Linux distro.