fix(mdns): publish via host avahi over D-Bus (not a second daemon)

A container running its own avahi-daemon collides with the host's on :5353.
Instead publish to the host avahi over its mounted D-Bus socket using avahi-tools
(avahi-publish). Needs apparmor=unconfined (docker-default blocks the D-Bus
publish). Documented host prereqs incl. allow-interfaces for multi-bridge hosts.

Tested: publish reaches host avahi and registers the names; on hosts with many
docker bridges avahi self-collides until restricted to the LAN interface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 06:03:26 +02:00
parent de75a2ca1d
commit c7151b7333
3 changed files with 35 additions and 20 deletions

View File

@@ -1,7 +1,14 @@
# mDNS toggle — publish *.local service names over Avahi for zero-config guest # mDNS toggle — publish *.local service names via the HOST's avahi-daemon for
# resolution. Enabled when ENABLE_MDNS=true in .env (build-stack layers it in) # zero-config guest resolution. Enabled when ENABLE_MDNS=true in .env and only
# and only meaningful when BASE_DOMAIN=*.local. Host networking is required for # meaningful when BASE_DOMAIN=*.local.
# link-local multicast. #
# Host prerequisites:
# - avahi-daemon running on the host
# - if the host has many interfaces (e.g. lots of docker bridges), restrict
# avahi to the LAN NIC in /etc/avahi/avahi-daemon.conf:
# allow-interfaces=<lan-iface>
# otherwise avahi sees its own announcements across bridges → "Local name
# collision" and publishing fails.
services: services:
avahi: avahi:
build: ./docker/avahi build: ./docker/avahi
@@ -9,6 +16,13 @@ services:
container_name: avahi container_name: avahi
restart: unless-stopped restart: unless-stopped
network_mode: host network_mode: host
# AppArmor's docker-default profile blocks the D-Bus publish to host avahi.
security_opt:
- apparmor=unconfined
environment: environment:
BASE_DOMAIN: ${BASE_DOMAIN} BASE_DOMAIN: ${BASE_DOMAIN}
HOST_LAN_IP: ${HOST_LAN_IP} HOST_LAN_IP: ${HOST_LAN_IP}
DBUS_SYSTEM_BUS_ADDRESS: "unix:path=/run/dbus/system_bus_socket"
volumes:
# Talk to the host's avahi-daemon (host must run avahi-daemon).
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket

View File

@@ -1,8 +1,9 @@
# Self-contained mDNS responder: publishes the stack's service names on .local # Publishes the stack's service names on .local via the HOST's avahi-daemon
# so guests resolve them with zero DNS/client config (macOS/Linux native; # (over its D-Bus socket — no second daemon, avoids the :5353 collision).
# Windows needs Bonjour). Used only when ENABLE_MDNS=true and BASE_DOMAIN=*.local. # Requires the host to run avahi-daemon and the socket to be mounted (see
# docker-compose.avahi.yml). Used when ENABLE_MDNS=true and BASE_DOMAIN=*.local.
FROM alpine:3.20 FROM alpine:3.20
RUN apk add --no-cache avahi dbus RUN apk add --no-cache avahi-tools
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# Publish A records for the stack's service names over mDNS (.local). # Publish A records for the stack's service names on .local via the HOST's
# Requires host networking (mDNS is link-local multicast). One avahi-publish # avahi-daemon (reached over its mounted D-Bus socket). One avahi-publish per
# per name; if any dies the container exits so Docker restarts it. # name; if any drops the container exits so Docker restarts it.
set -eu set -eu
: "${BASE_DOMAIN:?BASE_DOMAIN unset}" : "${BASE_DOMAIN:?BASE_DOMAIN unset}"
@@ -12,21 +12,21 @@ case "$BASE_DOMAIN" in
*) echo "WARN: mDNS resolves only .local — BASE_DOMAIN=$BASE_DOMAIN will NOT be discoverable via avahi. Set BASE_DOMAIN=*.local to use mDNS." >&2 ;; *) echo "WARN: mDNS resolves only .local — BASE_DOMAIN=$BASE_DOMAIN will NOT be discoverable via avahi. Set BASE_DOMAIN=*.local to use mDNS." >&2 ;;
esac esac
mkdir -p /run/dbus bus="${DBUS_SYSTEM_BUS_ADDRESS:-unix:path=/run/dbus/system_bus_socket}"
rm -f /run/dbus/pid /run/dbus/system_bus_socket 2>/dev/null || true sock=${bus#unix:path=}
dbus-daemon --system if [ ! -S "$sock" ]; then
for _ in 1 2 3 4 5; do [ -S /run/dbus/system_bus_socket ] && break; sleep 1; done echo "ERROR: host D-Bus socket '$sock' not found. Mount the host's" >&2
echo " /run/dbus/system_bus_socket and ensure avahi-daemon runs on the host." >&2
avahi-daemon --daemonize --no-chroot exit 1
sleep 2 fi
export DBUS_SYSTEM_BUS_ADDRESS="$bus"
pids="" pids=""
for n in "$BASE_DOMAIN" "auth.$BASE_DOMAIN" "packwiz.$BASE_DOMAIN" "mc.$BASE_DOMAIN"; do for n in "$BASE_DOMAIN" "auth.$BASE_DOMAIN" "packwiz.$BASE_DOMAIN" "mc.$BASE_DOMAIN"; do
echo "mDNS publish: $n -> $HOST_LAN_IP" echo "mDNS publish (host avahi): $n -> $HOST_LAN_IP"
avahi-publish -a "$n" "$HOST_LAN_IP" & avahi-publish -a "$n" "$HOST_LAN_IP" &
pids="$pids $!" pids="$pids $!"
done done
# Block on the publishers; exit (→ restart) if one drops.
# shellcheck disable=SC2086 # shellcheck disable=SC2086
wait -n $pids 2>/dev/null || wait wait -n $pids 2>/dev/null || wait