Summary
On macOS (and other non-Linux platforms) linkSubscribe is a no-op, so network
interface changes are never observed and services are never re-announced. Because
the responder has no other re-announcement path, a service can silently stop being
advertised while the process stays alive and healthy, with nothing logged.
This is the macOS counterpart to #9 and #3, both of which were closed as fixed in
v1.2.12. Those fixes are Linux-only, as noted in their closing comments:
I have to mention that observing changes to network interfaces use the netlink
API, which is only available on Linux. This means that this feature only works
on Linux.
I could not find an open issue tracking the non-Linux side, so filing one.
Why it does not recover on its own
Three things combine, and it is the combination rather than any one of them that
makes the failure permanent:
-
No link observation. netlink_others.go is the entire non-Linux
implementation:
//go:build !linux
func (r *responder) linkSubscribe(context.Context) {
log.Info.Println("dnssd: unable to wait for link updates")
}
The Linux build calls r.announce(services(r.managed)) on every link update.
The non-Linux build logs one line and returns.
-
No periodic re-announcement. After the two announcements sent at
registration, respond() is purely reactive: it answers queries and nothing
else. There is no ticker anywhere in responder.go or mdns.go that
re-announces on a schedule.
-
Multicast group membership is joined once. NewResponder() calls
newMDNSConn(), which does ListenUDP plus JoinGroup per interface at
construction, and that is the only place JoinGroup runs. When an interface
goes down the kernel drops its group membership, and nothing rejoins it.
So on macOS there is no mechanism, timed or event-driven, that can restore the
advertisement after a network change. The only recovery is restarting the process.
Point 3 also means a re-announce alone would not be a sufficient fix on this
platform: announcing through a socket whose membership has lapsed sends packets
nowhere. Any fix probably needs to rebuild the connection, not just re-announce
through the existing one.
Observed behaviour
Running brutella/hap as a long-lived HomeKit bridge on macOS, I have twice seen
the accessory vanish from the network while the process was still running
normally: the HAP TCP port still listening, all device connections healthy, and
dns-sd -B _hap._tcp local showing nothing on any real interface. Zero errors in
the application log or in dnssd's own output. A process restart fixed it both
times. I now run an external watchdog that browses for the advertisement every
five minutes and restarts the service when it is missing, which is not a great
place to be.
I want to be straightforward about the limits of this report: system logs had
already rolled over by the time I investigated, so I cannot say which network
event triggered either occurrence. The mechanism above is from reading the code,
and it is sufficient to explain the symptom, but I have not captured a trigger in
the act.
The host is dual-homed on one subnet (wired plus Wi-Fi), and also runs VPN tunnel
interfaces and bridged VM interfaces, so interface churn is frequent. That may be
why this shows up here more often than most setups.
Expected
Interface and address changes are observed on macOS as they are on Linux, and
affected services are re-announced.
Suggested approach
macOS and the BSDs expose a PF_ROUTE socket that delivers RTM_IFINFO,
RTM_NEWADDR and RTM_DELADDR messages on interface and address changes. It is
readable with plain golang.org/x/net/route and syscall, so this can be done
in pure Go with no cgo, as a netlink_darwin.go or netlink_bsd.go sibling to
the existing files, keeping the current stub as the fallback for other platforms.
Given point 3 above, it may also be worth having the update path rebuild the
mdnsConn (re-running JoinGroup) rather than only calling announce, on all
platforms. The Linux path may have the same lapsed-membership gap after an
interface bounce, though I have not tested that.
Happy to put together a PR for the PF_ROUTE watcher if that direction seems
right to you.
Environment
- dnssd
v1.2.14
- hap
v0.0.35
- go1.26.1 darwin/arm64
- macOS 26.2 (25C56), Apple silicon
Summary
On macOS (and other non-Linux platforms)
linkSubscribeis a no-op, so networkinterface changes are never observed and services are never re-announced. Because
the responder has no other re-announcement path, a service can silently stop being
advertised while the process stays alive and healthy, with nothing logged.
This is the macOS counterpart to #9 and #3, both of which were closed as fixed in
v1.2.12. Those fixes are Linux-only, as noted in their closing comments:I could not find an open issue tracking the non-Linux side, so filing one.
Why it does not recover on its own
Three things combine, and it is the combination rather than any one of them that
makes the failure permanent:
No link observation.
netlink_others.gois the entire non-Linuximplementation:
The Linux build calls
r.announce(services(r.managed))on every link update.The non-Linux build logs one line and returns.
No periodic re-announcement. After the two announcements sent at
registration,
respond()is purely reactive: it answers queries and nothingelse. There is no ticker anywhere in
responder.goormdns.gothatre-announces on a schedule.
Multicast group membership is joined once.
NewResponder()callsnewMDNSConn(), which doesListenUDPplusJoinGroupper interface atconstruction, and that is the only place
JoinGroupruns. When an interfacegoes down the kernel drops its group membership, and nothing rejoins it.
So on macOS there is no mechanism, timed or event-driven, that can restore the
advertisement after a network change. The only recovery is restarting the process.
Point 3 also means a re-announce alone would not be a sufficient fix on this
platform: announcing through a socket whose membership has lapsed sends packets
nowhere. Any fix probably needs to rebuild the connection, not just re-announce
through the existing one.
Observed behaviour
Running
brutella/hapas a long-lived HomeKit bridge on macOS, I have twice seenthe accessory vanish from the network while the process was still running
normally: the HAP TCP port still listening, all device connections healthy, and
dns-sd -B _hap._tcp localshowing nothing on any real interface. Zero errors inthe application log or in dnssd's own output. A process restart fixed it both
times. I now run an external watchdog that browses for the advertisement every
five minutes and restarts the service when it is missing, which is not a great
place to be.
I want to be straightforward about the limits of this report: system logs had
already rolled over by the time I investigated, so I cannot say which network
event triggered either occurrence. The mechanism above is from reading the code,
and it is sufficient to explain the symptom, but I have not captured a trigger in
the act.
The host is dual-homed on one subnet (wired plus Wi-Fi), and also runs VPN tunnel
interfaces and bridged VM interfaces, so interface churn is frequent. That may be
why this shows up here more often than most setups.
Expected
Interface and address changes are observed on macOS as they are on Linux, and
affected services are re-announced.
Suggested approach
macOS and the BSDs expose a
PF_ROUTEsocket that deliversRTM_IFINFO,RTM_NEWADDRandRTM_DELADDRmessages on interface and address changes. It isreadable with plain
golang.org/x/net/routeandsyscall, so this can be donein pure Go with no cgo, as a
netlink_darwin.goornetlink_bsd.gosibling tothe existing files, keeping the current stub as the fallback for other platforms.
Given point 3 above, it may also be worth having the update path rebuild the
mdnsConn(re-runningJoinGroup) rather than only callingannounce, on allplatforms. The Linux path may have the same lapsed-membership gap after an
interface bounce, though I have not tested that.
Happy to put together a PR for the
PF_ROUTEwatcher if that direction seemsright to you.
Environment
v1.2.14v0.0.35