Skip to content

Announce services after a reboot on networks with an mDNS reflector - #36

Open
mguaylam wants to merge 2 commits into
openwrt:masterfrom
mguaylam:announce-after-reflected-probe
Open

mguaylam wants to merge 2 commits into
openwrt:masterfrom
mguaylam:announce-after-reflected-probe

Conversation

@mguaylam

Copy link
Copy Markdown

On a router behind a network that runs an mDNS reflector, services registered through procd_add_mdns were never announced after a reboot. A Sendspin audio player announced this way was not rediscovered by its server for over ten minutes, until the server happened to query again. Two independent issues combine; one commit each.

1. cache: our own address records are taken for a probe conflict

With umdns -d, umdns receives its own ANY OpenWrt.local probe and its own answers back from the reflector, caches A OpenWrt.local with its own address, and at STATE_PROBE_END:

the host OpenWrt.local already exists. stopping announce service

It then never announces anything on that interface. RFC 6762 §9 only treats records whose data differs as conflicts, so cached address records carrying one of our own interface addresses are now ignored.

This is complementary to #33, which renames the host when a conflict is detected: on such a network, #33 alone would rename the host because of its own echo.

2. announce: service instances are not announced when an interface comes up

Since a2b4979 ("service: announce all services in single dns answer"), service_announce_services() sends only the service type PTRs. An instance is announced when it is added, but if that happens before its interface has an address (a DHCP lease arriving after the service starts at boot), the instance PTR/SRV/TXT records are never announced. RFC 6762 §8 and §8.3 ask for all records to be announced on startup and link change. They are now sent once, when probing succeeds, and are not added to the periodic STATE_ANNOUNCE refresh.

Testing

OpenWrt 25.12.5, ramips/mt7621, umdns announcing on a DHCP-configured bridge, network with an mDNS reflector, Music Assistant as the Sendspin server:

umdns server reconnects after a router reboot
25.12.5 package (1b5e7bf1) not after 11 min
with commit 2 only not after 5 min (still stops at probing)
with both commits 5 s after the interface got its address

Both commits apply to and build on current master (dbee550) with the project's -Werror, cross-compiled with the 25.12.5 SDK.

At the end of probing, cache_host_is_known() treats any cached A or AAAA
record for our host name as a conflict, and umdns stops announcing on
that interface for good.

On a network with an mDNS reflector, umdns receives its own probe and
its own answers back, caches them, and so finds a conflict with itself:

  the host OpenWrt.local already exists. stopping announce service

It then never announces its host or services on that interface.

RFC 6762 section 9 only treats records with different data as a
conflict. Ignore cached address records that carry one of our own
interface addresses.

Signed-off-by: Michaël Guay-Lambert <michaelguaylambert@me.com>
Since commit a2b4979 ("service: announce all services in single dns
answer"), service_announce_services() only sends the service type PTR
records, so an interface coming up announces the host records and the
service types but not the service instances themselves (instance PTR,
SRV and TXT).

A service registered before its interface has an address, as happens at
boot when a DHCP lease arrives after the service starts, is therefore
never announced, and a browser that already dropped it only finds it
again on its next query.

RFC 6762 section 8.3 requires announcing all newly registered records.
Send the service instances once probing succeeds, without adding them to
the periodic refresh of STATE_ANNOUNCE.

Signed-off-by: Michaël Guay-Lambert <michaelguaylambert@me.com>
@mguaylam mguaylam reopened this Sep 14, 2026
mguaylam added a commit to mguaylam/packages that referenced this pull request Sep 22, 2026
Sendspin is the synchronized multi-room audio protocol used by Music
Assistant, and sendspin-cli is its headless player. It plays through
ALSA, so a router with a USB DAC becomes a player in the same way
snapcast and shairport-sync already allow here.

Upstream builds its dependencies with CMake FetchContent. Rather than
patch that out, each archive is downloaded through its own Download
block and handed to CMake with FETCHCONTENT_SOURCE_DIR_<NAME>, with
FETCHCONTENT_FULLY_DISCONNECTED=ON so the build never reaches the
network. micro-opus, which bundles a copy of Opus, is replaced by a
small CMake shim that links the feed's libopus through pkg-config.
The package therefore carries no patches and no vendored code.

The init script is procd-based and validates its UCI section with
uci_load_validate. The player is built without mDNS and announced by
umdns through procd_add_mdns instead.

Two behaviours are worth explaining rather than leaving to be found:

service_stopped() waits for the player to exit and then reloads umdns.
The player says goodbye with reason 'shutdown' whenever it stops, and a
server reconnects only once mDNS announces it again; umdns announces a
service when it appears and withdraws it when it disappears, but a
restart that swaps a service for an identical one does neither.

The iface hotplug script restarts the player when a network umdns
announces on comes up. The umdns in 25.12 does not announce service
instances when a network comes up, and on a network with an mDNS
reflector it takes its own reflected probe for a name conflict and
stops announcing, so the player is not rediscovered after a reboot.
Both are addressed by a pending pull request, openwrt/mdnsd#36; the
script goes away when it ships.

The package is not offered on big-endian targets. Software volume, the
Opus decoder and micro-flac's sample packing all write samples in host
byte order into buffers the ALSA sink opens as _LE, so playback there
would be wrong rather than merely imperfect. All three are reproduced
under qemu-mips-static and reported upstream; the guard is one line and
comes off when they are fixed.

Tested on ramips/mt7621 (D-Link DIR-3040, mipsel_24kc) with a USB DAC:
over 49 hours of uninterrupted playback, no XRUN or underrun, 1.45 MiB
installed, 5 MiB resident, and 6.3% of one 880 MHz thread decoding
FLAC 48 kHz/16-bit with software volume. The in-place upgrade path and
config preservation were verified on the same device.

Signed-off-by: Michaël Guay-Lambert <michaelguaylambert@me.com>
mguaylam added a commit to mguaylam/packages that referenced this pull request Sep 22, 2026
Sendspin is the synchronized multi-room audio protocol used by Music
Assistant, and sendspin-cli is its headless player. It plays through
ALSA, so a router with a USB DAC becomes a player in the same way
snapcast and shairport-sync already allow here.

Upstream builds its dependencies with CMake FetchContent. Rather than
patch that out, each archive is downloaded through its own Download
block and handed to CMake with FETCHCONTENT_SOURCE_DIR_<NAME>, with
FETCHCONTENT_FULLY_DISCONNECTED=ON so the build never reaches the
network. micro-opus, which bundles a copy of Opus, is replaced by a
small CMake shim that links the feed's libopus through pkg-config.
The package therefore carries no patches and no vendored code.

The init script is procd-based and validates its UCI section with
uci_load_validate. The player is built without mDNS and announced by
umdns through procd_add_mdns instead.

Two behaviours are worth explaining rather than leaving to be found:

service_stopped() waits for the player to exit and then reloads umdns.
The player says goodbye with reason 'shutdown' whenever it stops, and a
server reconnects only once mDNS announces it again; umdns announces a
service when it appears and withdraws it when it disappears, but a
restart that swaps a service for an identical one does neither.

The iface hotplug script restarts the player when a network umdns
announces on comes up. The umdns in 25.12 does not announce service
instances when a network comes up, and on a network with an mDNS
reflector it takes its own reflected probe for a name conflict and
stops announcing, so the player is not rediscovered after a reboot.
Both are addressed by a pending pull request, openwrt/mdnsd#36; the
script goes away when it ships.

The package is not offered on big-endian targets. Software volume, the
Opus decoder and micro-flac's sample packing all write samples in host
byte order into buffers the ALSA sink opens as _LE, so playback there
would be wrong rather than merely imperfect. All three are reproduced
under qemu-mips-static and reported upstream; the guard is one line and
comes off when they are fixed.

Tested on ramips/mt7621 (D-Link DIR-3040, mipsel_24kc) with a USB DAC:
over 49 hours of uninterrupted playback, no XRUN or underrun, 1.45 MiB
installed, 5 MiB resident, and 6.3% of one 880 MHz thread decoding
FLAC 48 kHz/16-bit with software volume. The in-place upgrade path and
config preservation were verified on the same device.

Signed-off-by: Michaël Guay-Lambert <mguaylam.dev@gmail.com>
mguaylam added a commit to mguaylam/packages that referenced this pull request Sep 22, 2026
Sendspin is the synchronized multi-room audio protocol used by Music
Assistant, and sendspin-cli is its headless player. It plays through
ALSA, so a router with a USB DAC becomes a player in the same way
snapcast and shairport-sync already allow here.

Upstream builds its dependencies with CMake FetchContent. Rather than
patch that out, each archive is downloaded through its own Download
block and handed to CMake with FETCHCONTENT_SOURCE_DIR_<NAME>, with
FETCHCONTENT_FULLY_DISCONNECTED=ON so the build never reaches the
network. micro-opus, which bundles a copy of Opus, is replaced by a
small CMake shim that links the feed's libopus through pkg-config.
The package therefore carries no patches and no vendored code.

The init script is procd-based and validates its UCI section with
uci_load_validate. The player is built without mDNS and announced by
umdns through procd_add_mdns instead.

Two behaviours are worth explaining rather than leaving to be found:

service_stopped() waits for the player to exit and then reloads umdns.
The player says goodbye with reason 'shutdown' whenever it stops, and a
server reconnects only once mDNS announces it again; umdns announces a
service when it appears and withdraws it when it disappears, but a
restart that swaps a service for an identical one does neither.

The iface hotplug script restarts the player when a network umdns
announces on comes up. The umdns in 25.12 does not announce service
instances when a network comes up, and on a network with an mDNS
reflector it takes its own reflected probe for a name conflict and
stops announcing, so the player is not rediscovered after a reboot.
Both are addressed by a pending pull request, openwrt/mdnsd#36; the
script goes away when it ships.

The package is not offered on big-endian targets. Software volume, the
Opus decoder and micro-flac's sample packing all write samples in host
byte order into buffers the ALSA sink opens as _LE, so playback there
would be wrong rather than merely imperfect. All three are reproduced
under qemu-mips-static and reported upstream; the guard is one line and
comes off when they are fixed.

Tested on ramips/mt7621 (D-Link DIR-3040, mipsel_24kc) with a USB DAC:
over 49 hours of uninterrupted playback, no XRUN or underrun, 1.45 MiB
installed, 5 MiB resident, and 6.3% of one 880 MHz thread decoding
FLAC 48 kHz/16-bit with software volume. The in-place upgrade path and
config preservation were verified on the same device.

Signed-off-by: Michaël Guay-Lambert <mguaylam.dev@gmail.com>
mguaylam added a commit to mguaylam/packages that referenced this pull request Sep 22, 2026
Sendspin is the synchronized multi-room audio protocol used by Music
Assistant, and sendspin-cli is its headless player. It plays through
ALSA, so a router with a USB DAC becomes a player in the same way
snapcast and shairport-sync already allow here.

Upstream builds its dependencies with CMake FetchContent. Rather than
patch that out, each archive is downloaded through its own Download
block and handed to CMake with FETCHCONTENT_SOURCE_DIR_<NAME>, with
FETCHCONTENT_FULLY_DISCONNECTED=ON so the build never reaches the
network. micro-opus, which bundles a copy of Opus, is replaced by a
small CMake shim that links the feed's libopus through pkg-config.
The package therefore carries no patches and no vendored code.

The init script is procd-based and validates its UCI section with
uci_load_validate. The player is built without mDNS and announced by
umdns through procd_add_mdns instead.

Two behaviours are worth explaining rather than leaving to be found:

service_stopped() waits for the player to exit and then reloads umdns.
The player says goodbye with reason 'shutdown' whenever it stops, and a
server reconnects only once mDNS announces it again; umdns announces a
service when it appears and withdraws it when it disappears, but a
restart that swaps a service for an identical one does neither.

The iface hotplug script restarts the player when a network umdns
announces on comes up. The umdns in 25.12 does not announce service
instances when a network comes up, and on a network with an mDNS
reflector it takes its own reflected probe for a name conflict and
stops announcing, so the player is not rediscovered after a reboot.
Both are addressed by a pending pull request, openwrt/mdnsd#36; the
script goes away when it ships.

The package is not offered on big-endian targets. Software volume, the
Opus decoder and micro-flac's sample packing all write samples in host
byte order into buffers the ALSA sink opens as _LE, so playback there
would be wrong rather than merely imperfect. All three are reproduced
under qemu-mips-static and reported upstream; the guard is one line and
comes off when they are fixed.

Tested on ramips/mt7621 (D-Link DIR-3040, mipsel_24kc) with a USB DAC:
over 49 hours of uninterrupted playback, no XRUN or underrun, 1.45 MiB
installed, 5 MiB resident, and 6.3% of one 880 MHz thread decoding
FLAC 48 kHz/16-bit with software volume. The in-place upgrade path and
config preservation were verified on the same device.

Signed-off-by: Michaël Guay-Lambert <mguaylam.dev@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant