Repository navigation
Add native DNS-SD advertiser for macOS (no UDP 5353 bind) #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # Native DNS-SD / Bonjour advertiser | ||
|
|
||
| ## Problem | ||
|
|
||
| HAP-Java historically advertises `_hap._tcp` with **JmDNS**, which opens a multicast UDP socket on **port 5353**. | ||
|
|
||
| On **macOS**, Apple’s `mDNSResponder` already owns `*:5353`. A second process (or a JVM) that tries to bind 5353 gets `Address already in use`, and Home / Controllers never see the accessory — even when the HAP TCP port (e.g. 9124) is listening. | ||
|
|
||
| Typical symptoms on macOS: empty `dns-sd -B _hap._tcp` results while `lsof -iUDP:5353` only lists `mDNSResponder`. | ||
|
|
||
| ## Solution | ||
|
|
||
| This fork adds a pluggable **`HomekitAdvertiser`** and a native implementation **`DnssdHomekitAdvertiser`** that calls Apple’s DNS-SD API (`DNSServiceRegister`) via JNA: | ||
|
|
||
| | Implementation | Class | Binds UDP 5353? | Typical platform | | ||
| | --- | --- | --- | --- | | ||
| | JmDNS (default / classic) | `JmdnsHomekitAdvertiser` | Yes (unless sharing an existing `JmDNS`) | Linux containers, Windows | | ||
| | Native DNS-SD | `DnssdHomekitAdvertiser` | **No** — registers through the system daemon | **macOS** (mDNSResponder), Linux with Avahi `libdns_sd` | | ||
|
|
||
| TXT records (`sf`, `id`, `md`, `sh`, `c#`, `s#`, `ff`, `ci`, `pv`) are shared via `HapDnsTxt` so both advertisers publish the same HAP payload. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```java | ||
| import io.github.hapjava.server.HomekitAdvertisers; | ||
| import io.github.hapjava.server.impl.HomekitServer; | ||
| import java.net.InetAddress; | ||
|
|
||
| InetAddress bind = InetAddress.getByName("192.168.0.6"); | ||
| HomekitServer server = | ||
| new HomekitServer(bind, 9124, HomekitAdvertisers.nativeDnssd()); | ||
|
|
||
| // or: prefer native when present, else JmDNS | ||
| HomekitServer auto = | ||
| new HomekitServer(bind, 9124, HomekitAdvertisers.auto(bind)); | ||
| ``` | ||
|
|
||
| Check availability: | ||
|
|
||
| ```java | ||
| if (HomekitAdvertisers.isNativeDnssdSupported()) { | ||
| // safe to call nativeDnssd() | ||
| } | ||
| ``` | ||
|
|
||
| Existing constructors that take only `InetAddress` / `JmDNS` keep the previous JmDNS behaviour (backward compatible). | ||
|
|
||
| ## Verifying on macOS | ||
|
|
||
| After starting a server with `HomekitAdvertisers.nativeDnssd()` (or `auto(...)`): | ||
|
|
||
| ```bash | ||
| dns-sd -B _hap._tcp local. | ||
| # or | ||
| dns-sd -Z _hap._tcp local. | ||
| ``` | ||
|
|
||
| You should see the bridge/accessory instance. No Java process should be listening on UDP 5353 (`lsof -iUDP:5353` should only show `mDNSResponder`). | ||
|
|
||
| ## API surface | ||
|
|
||
| ### `io.github.hapjava.server.HomekitAdvertiser` | ||
|
|
||
| ```text | ||
| advertise(label, category, mac, port, configurationIndex, setupId) | ||
| stop() | ||
| setDiscoverable(boolean) | ||
| setMac(String) | ||
| setConfigurationIndex(int) | ||
| ``` | ||
|
|
||
| ### `io.github.hapjava.server.HomekitAdvertisers` | ||
|
|
||
| | Factory | Behaviour | | ||
| | --- | --- | | ||
| | `jmdns(JmDNS)` | Wrap an existing JmDNS | | ||
| | `jmdns(InetAddress)` | Create JmDNS on that address | | ||
| | `nativeDnssd()` | Native Bonjour / dns_sd (throws if unsupported) | | ||
| | `auto(InetAddress)` | Native if supported, else JmDNS | | ||
| | `isNativeDnssdSupported()` | Probe without constructing | | ||
|
|
||
| ### `HomekitServer` constructors | ||
|
|
||
| ```java | ||
| new HomekitServer(InetAddress localAddress, int port, HomekitAdvertiser advertiser) | ||
| new HomekitServer(InetAddress localAddress, int port, int nThreads, HomekitAdvertiser advertiser) | ||
| ``` | ||
|
|
||
| When an advertiser is supplied, `createBridge` / `createStandaloneAccessory` use it and do **not** open a private JmDNS socket. | ||
|
|
||
| ## Implementation notes | ||
|
|
||
| * **JNA** loads `System` on macOS/Darwin and `dns_sd` elsewhere (Avahi Bonjour compatibility). | ||
| * `DNSServiceRegister` receives the TCP port in **network byte order**. | ||
| * TXT is RFC 6763 length-prefixed `key=value` bytes (`HapDnsTxt.encodeTxt`). | ||
| * A daemon thread runs `DNSServiceProcessResult` until `DNSServiceRefDeallocate` on `stop()` / re-register. | ||
| * Re-registration (discoverability, MAC, `c#`) deallocates and registers again — same pattern as the JmDNS advertiser. | ||
|
|
||
| ## Tests | ||
|
|
||
| | Test | Purpose | | ||
| | --- | --- | | ||
| | `HapDnsTxtTest` | Required TXT keys + wire encoding | | ||
| | `DnssdHomekitAdvertiserTest` | Mocked JNA register / re-register / stop / port endianness | | ||
| | `JmdnsHomekitAdvertiserTest` | Existing JmDNS behaviour (unchanged) | | ||
| | `HomekitAdvertisersTest` | Factory / support probe | | ||
| | `DnssdLiveAdvertiserIT` | Real register+stop when native DNS-SD is present | | ||
|
|
||
| ```bash | ||
| mvn test | ||
| ``` | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| | Symptom | Likely cause | | ||
| | --- | --- | | ||
| | `Address already in use` / “no suitable mDNS” | Still using JmDNS against mDNSResponder — switch to `nativeDnssd()` | | ||
| | `UnsupportedOperationException: Native DNS-SD` | No `libSystem`/`libdns_sd` on this JVM/OS | | ||
| | `DNSServiceRegister failed with error code …` | Bad name/type, permissions, or daemon unavailable — check Console / `mDNSResponder` | | ||
| | Browse empty after register | Wrong interface, firewall, or advertiser stopped; confirm with `dns-sd -B _hap._tcp` | | ||
|
|
||
| ## License | ||
|
|
||
| Same MIT license as HAP-Java. Native symbols are provided by the host OS; this library only calls them through JNA. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package io.github.hapjava.server; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| /** | ||
| * Publishes a HomeKit accessory / bridge via DNS-SD ({@code _hap._tcp}). | ||
| * | ||
| * <p>The default implementation uses JmDNS and binds UDP port 5353. On platforms where a system | ||
| * mDNS responder already owns that port (notably macOS {@code mDNSResponder}), use {@link | ||
| * io.github.hapjava.server.impl.dnssd.DnssdHomekitAdvertiser} instead, which registers through the | ||
| * native Bonjour / DNS-SD API without binding 5353. | ||
| * | ||
| * @see io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser | ||
| * @see io.github.hapjava.server.impl.dnssd.DnssdHomekitAdvertiser | ||
| * @see HomekitAdvertisers | ||
| * @author Karel Goderis | ||
| */ | ||
| public interface HomekitAdvertiser { | ||
|
|
||
| /** | ||
| * Start advertising the accessory. | ||
| * | ||
| * @param label instance name shown in Home / Discovery | ||
| * @param category HomeKit accessory category ({@code ci} TXT key) | ||
| * @param mac accessory id ({@code id} TXT key), typically a MAC-like string | ||
| * @param port HAP TCP listen port | ||
| * @param configurationIndex configuration number ({@code c#} TXT key) | ||
| * @param setupId setup id used for the {@code sh} TXT hash | ||
| */ | ||
| void advertise( | ||
| String label, int category, String mac, int port, int configurationIndex, String setupId) | ||
| throws Exception; | ||
|
|
||
| /** Stop advertising and release native resources. */ | ||
| void stop(); | ||
|
|
||
| /** Update discoverability ({@code sf} TXT). Re-registers when already advertising. */ | ||
| void setDiscoverable(boolean discoverable) throws IOException; | ||
|
|
||
| /** Update accessory id ({@code id} TXT). Re-registers when already advertising. */ | ||
| void setMac(String mac) throws IOException; | ||
|
|
||
| /** Update configuration index ({@code c#} TXT). Re-registers when already advertising. */ | ||
| void setConfigurationIndex(int revision) throws IOException; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package io.github.hapjava.server; | ||
|
|
||
| import io.github.hapjava.server.impl.dnssd.DnssdHomekitAdvertiser; | ||
| import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser; | ||
| import java.io.IOException; | ||
| import java.net.InetAddress; | ||
| import javax.jmdns.JmDNS; | ||
|
|
||
| /** | ||
| * Factories for {@link HomekitAdvertiser} implementations. | ||
| * | ||
| * <p>Typical macOS usage: | ||
| * | ||
| * <pre>{@code | ||
| * HomekitServer server = | ||
| * new HomekitServer(InetAddress.getByName("192.168.0.6"), 9124, HomekitAdvertisers.nativeDnssd()); | ||
| * }</pre> | ||
| * | ||
| * That listens on HAP TCP without binding UDP 5353; Bonjour/mDNSResponder publishes {@code | ||
| * _hap._tcp}. | ||
| * | ||
| * @author Karel Goderis | ||
| */ | ||
| public final class HomekitAdvertisers { | ||
|
|
||
| private HomekitAdvertisers() {} | ||
|
|
||
| /** | ||
| * Classic JmDNS advertiser (binds UDP 5353 unless sharing an existing {@link JmDNS}). | ||
| * | ||
| * @param jmdns existing JmDNS instance to register with | ||
| * @return a JmDNS-backed advertiser | ||
| */ | ||
| public static HomekitAdvertiser jmdns(JmDNS jmdns) { | ||
| return new JmdnsHomekitAdvertiser(jmdns); | ||
| } | ||
|
|
||
| /** | ||
| * Classic JmDNS advertiser that creates its own multicast socket on {@code localAddress}. | ||
| * | ||
| * @param localAddress address used to create the JmDNS instance | ||
| * @return a JmDNS-backed advertiser | ||
| * @throws IOException if JmDNS cannot be created on {@code localAddress} | ||
| */ | ||
| public static HomekitAdvertiser jmdns(InetAddress localAddress) throws IOException { | ||
| return new JmdnsHomekitAdvertiser(localAddress); | ||
| } | ||
|
|
||
| /** | ||
| * Native DNS-SD / Bonjour advertiser (macOS {@code mDNSResponder}, or Avahi's dns_sd compat | ||
| * layer). Does not bind UDP 5353. Requires the JNA runtime dependency. | ||
| * | ||
| * @return a native DNS-SD advertiser | ||
| * @throws UnsupportedOperationException if the native library cannot be loaded | ||
| */ | ||
| public static HomekitAdvertiser nativeDnssd() { | ||
| if (!DnssdHomekitAdvertiser.isSupported()) { | ||
| throw new UnsupportedOperationException( | ||
| "Native DNS-SD (dns_sd / Bonjour) is not available on this platform/JVM"); | ||
| } | ||
| return new DnssdHomekitAdvertiser(); | ||
| } | ||
|
|
||
| /** | ||
| * Prefer native DNS-SD when available (macOS); otherwise create a dedicated JmDNS instance on | ||
| * {@code localAddress}. | ||
| * | ||
| * @param localAddress address used if falling back to JmDNS | ||
| * @return a native DNS-SD advertiser when supported, otherwise a JmDNS-backed advertiser | ||
| * @throws IOException if native DNS-SD is unavailable and JmDNS cannot be created | ||
| */ | ||
| public static HomekitAdvertiser auto(InetAddress localAddress) throws IOException { | ||
| if (DnssdHomekitAdvertiser.isSupported()) { | ||
| return new DnssdHomekitAdvertiser(); | ||
| } | ||
| return new JmdnsHomekitAdvertiser(localAddress); | ||
|
Comment on lines
+104
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i would prefer to able to force jmDNS usage, even on linux or macosx library. i.e. i could use share jmDNS instance from openHab. it should be configurable in openHAB and not automatic decision in Java HAP. |
||
| } | ||
|
|
||
| /** | ||
| * Whether {@link #nativeDnssd()} can be used on this JVM/OS. | ||
| * | ||
| * @return {@code true} if the platform DNS-SD library loaded successfully | ||
| */ | ||
| public static boolean isNativeDnssdSupported() { | ||
| return DnssdHomekitAdvertiser.isSupported(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes JNA (~1.9 MB, native access) a compile-scope transitive dependency for every HAP-Java consumer, including everyone who only uses the JmDNS path. mvn dependency:tree shows net.java.dev.jna:jna:jar:5.14.0:compile.
Since native DNS-SD is opt-in, can this be optional? Consumers who want the native advertiser then declare JNA themselves. openhab-addons#22013 already embeds JNA in the bundle, so nothing changes for openHAB.
i.e.
net.java.dev.jna
jna
5.14.0
true