Madness.Responder.MDNSResponder (Madness v0.5.0)

View Source

Publishes through Apple's mDNSResponder, by way of its Unix-socket IPC.

This is the backend for macOS, where the daemon is always running and cannot practically be turned off, and for anything else running Apple's daemon — notably a Nerves target that bundles the POSIX mdnsd. Automatic selection probes it after Avahi; name it explicitly to use only this backend:

config :madness, backend: :mdns_responder

One socket per registration

Each publication opens its own connection to the daemon and keeps it for exactly as long as the registration should live. Nothing is pooled and nothing is shared, including with the presence connection that Madness's connection process holds.

The daemon withdraws a client's records when its socket closes, so a service is revoked even if its publication process is killed without cleanup.

Interface scoping, and the absence of family scoping

Madness.Responder.capabilities/0 declares :interface_scope and nothing else. The interface_index field of a registration restricts probing, announcing and answering to one interface, and madness fills it with the OS index resolved immediately before the call.

Apple's IPC has no per-address-family control, so Madness.publish/2 refuses a family scope on this backend. family: :any is the portable choice; Avahi can narrow it.

Service subtypes

Madness.Service represents a subtype by its DNS name, such as "_printer._sub._http._tcp". Apple's registration API spells the same request as "_http._tcp,_printer"; this backend performs that conversion when it builds the IPC request. Plain service types are sent unchanged.

Conflicts and the accepted name

Registrations are made with auto-rename left on. When the requested name is already taken, the daemon appends a discriminator — "My Printer (2)" — and reports the name it settled on in an asynchronous reply, which is what Madness.Responder.publish/3 returns as the accepted name.

Publishing waits a bounded time for the reply and otherwise reports the requested name; the daemon may merge an identical registration without a callback.

A rename can also happen long after publishing, when a conflicting host appears on the network. Each registration socket is drained for the life of the publication, so a later rename is delivered to the publication as a {:renamed, name} event and reaches the caller of Madness.publish/2 when it asked for notify: true.

When the daemon restarts

A registration lives in the daemon, so a daemon that restarts loses it. That arrives here as the registration socket closing, and is reported to the publication as {:error, {:connection_lost, reason}} — again reaching the caller when it asked for notify: true. Madness then terminates the publication with reason {:shutdown, {:connection_lost, reason}}; it does not guess whether that publication is still wanted or should move to a replacement network. Monitor the pid in the returned Madness.Publication and publish explicitly again if the service should return. The node's separate presence connection reconnects on its own and makes publishing available again when the daemon is reachable.

Configuration

  • DNSSD_UDS_PATH - an environment variable naming the daemon socket, honoured by Apple's own client library and useful for pointing madness at a daemon somewhere other than the standard location. When it is set it replaces the default socket search.

    Otherwise /var/run/mDNSResponder is tried first, then /var/run/mdnsd, which is where the stock POSIX build listens.

  • config :madness, mdnsresponder_reply_timeout: 2_500 - how long Madness.Responder.publish/3 waits for the reply carrying the accepted name before falling back to the requested one.

  • config :madness, mdnsresponder_publish_budget: 4_000 - the maximum time for the complete backend registration. The caller's timeout: may shorten this budget but never extends it.

Summary

Types

The presence connection held by Madness's connection process

One registration, and the socket whose closure revokes it

Functions

The scopes this backend can enforce: :interface_scope only.

Opens the presence connection.

Closes the presence connection.

Interprets a message delivered for the presence connection.

Registers service, on its own connection, owned by the calling process.

Withdraws a registration by closing its connection.

Types

connection()

@opaque connection()

The presence connection held by Madness's connection process

handle()

@opaque handle()

One registration, and the socket whose closure revokes it

Functions

capabilities()

@spec capabilities() :: [Madness.Responder.capability()]

The scopes this backend can enforce: :interface_scope only.

See "Interface scoping, and the absence of family scoping" above for why :family_scope is not among them.

connect()

@spec connect() :: {:ok, connection()} | {:error, term()}

Opens the presence connection.

Connecting alone would only prove that something is bound to the socket path, so this also asks the daemon for its DaemonVersion and requires an answer. The value itself is not interpreted — it is a build identifier whose encoding is Apple's business, and madness has no version policy to apply to it. What matters is that a request went in and a well-formed reply came back, which a stale socket file or a wedged daemon cannot produce.

The connection is then left open and switched to active mode, so that the daemon going away arrives as a message rather than having to be polled for.

disconnect(arg1)

@spec disconnect(connection()) :: :ok

Closes the presence connection.

handle_info(arg1, connection)

@spec handle_info(term(), connection()) :: {:ok, connection()} | :disconnected

Interprets a message delivered for the presence connection.

The connection carries no traffic once it is established — nothing is subscribed to it — so what this is really watching for is its closure, which is how the daemon exiting becomes visible. Anything else is ignored rather than treated as a fault: a stray message is not evidence the daemon has gone.

publish(service, scope, opts)

@spec publish(Madness.Service.t(), Madness.Responder.scope(), [
  Madness.Responder.publish_opt()
]) ::
  {:ok, handle(), String.t()} | {:error, term()}

Registers service, on its own connection, owned by the calling process.

Every wait inside the call is bounded by the earlier of two moments: the end of this backend's own publish budget, and the :deadline in opts if one is given. So a caller who will stop waiting sooner is answered sooner, and one who would wait forever is still bounded by the budget. See "Configuration".

unpublish(arg1)

@spec unpublish(handle()) :: :ok

Withdraws a registration by closing its connection.

Closing is the whole of it: the daemon drops the records and multicasts the goodbyes. That makes this both prompt — a close does not wait on the daemon — and idempotent, since closing an already-closed socket is a no-op.