Skip to content

Network interface changes are not observed on macOS, leaving services silently unadvertised #65

Description

@josephspurrier

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:

  1. 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.

  2. 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.

  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions