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 all commits
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,139 @@ | ||
| # 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 library 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. | ||
|
|
||
| **Policy belongs in the application** (e.g. openHAB): choose `jmdns(...)` or `nativeDnssd(...)` explicitly. Do not rely on HAP-Java to auto-pick. | ||
|
|
||
| ## Quick start | ||
|
|
||
| Declare the optional JNA dependency if you use the native advertiser (JmDNS-only consumers can omit it): | ||
|
|
||
| ```xml | ||
| <dependency> | ||
| <groupId>net.java.dev.jna</groupId> | ||
| <artifactId>jna</artifactId> | ||
| <version>5.14.0</version> | ||
| </dependency> | ||
| ``` | ||
|
|
||
| ```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"); | ||
|
|
||
| // Prefer scoping native registration to the HAP bind interface (SRV host stays system default): | ||
| HomekitServer server = | ||
| new HomekitServer(bind, 9124, HomekitAdvertisers.nativeDnssd(bind)); | ||
|
|
||
| // Or share an existing JmDNS (e.g. openHAB's): | ||
| // HomekitServer server = | ||
| // new HomekitServer(bind, 9124, HomekitAdvertisers.jmdns(sharedJmDNS)); | ||
| ``` | ||
|
|
||
| Check whether the native library **loads** (does not prove registration will succeed on the LAN): | ||
|
|
||
| ```java | ||
| if (HomekitAdvertisers.isNativeDnssdSupported()) { | ||
| // safe to construct nativeDnssd(...) | ||
| } | ||
| ``` | ||
|
|
||
| Existing constructors that take only `InetAddress` / `JmDNS` keep the previous JmDNS behaviour (backward compatible). | ||
|
|
||
| ## Verifying on macOS (manual) | ||
|
|
||
| There is no automated live IT in CI (ubuntu runners lack `libdns_sd` / Bonjour). After starting a server with `HomekitAdvertisers.nativeDnssd(bind)`: | ||
|
|
||
| ```bash | ||
| dns-sd -B _hap._tcp local. | ||
| dns-sd -L "<instance name>" _hap._tcp local. | ||
| # or | ||
| dns-sd -Z _hap._tcp local. | ||
| ``` | ||
|
|
||
| You should see the bridge/accessory instance and HAP TXT keys. 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 (force JmDNS / share openHAB’s instance) | | ||
| | `jmdns(InetAddress)` | Create dedicated JmDNS on that address | | ||
| | `nativeDnssd()` | Native Bonjour on **all** interfaces (`interfaceIndex = 0`, default host) | | ||
| | `nativeDnssd(InetAddress)` | Native Bonjour on that address’s interface index; SRV host = system default (`null`) | | ||
| | `isNativeDnssdSupported()` | Whether JNA + dns_sd **load** (not a live register probe) | | ||
| | `auto(InetAddress)` | **Deprecated** — prefer an explicit `jmdns` / `nativeDnssd` choice | | ||
|
|
||
| ### `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** is an **optional** dependency. It loads `System` on macOS/Darwin and `dns_sd` elsewhere (Avahi Bonjour compatibility). Missing JNA surfaces as `LinkageError` and `isNativeDnssdSupported()` returns `false`. | ||
| * `DNSServiceRegister` receives the TCP port in **network byte order**. | ||
| * Default `nativeDnssd()` uses `interfaceIndex = 0` (all interfaces) and `host = null` (system hostname). Prefer `nativeDnssd(InetAddress)` to limit the interface index when the HAP TCP listener is bound to one LAN address — still with `host = null`. Never pass a raw IP as DNS-SD `host` (no A record → Home hangs on “Setting up”). | ||
| * TXT is RFC 6763 length-prefixed `key=value` bytes (`HapDnsTxt.encodeTxt`). Key insertion order is stable (`LinkedHashMap`); RFC 6763 does not require a particular pair order. | ||
| * 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` | Full TXT key set, stable order, wire encoding | | ||
| | `DnssdHomekitAdvertiserTest` | Mocked JNA register / interface+host / re-register / stop / port endianness | | ||
| | `JmdnsHomekitAdvertiserTest` | JmDNS register including full HAP property set | | ||
|
|
||
| ```bash | ||
| mvn test | ||
| ``` | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| | Symptom | Likely cause | | ||
| | --- | --- | | ||
| | `Address already in use` / “no suitable mDNS” | Still using dedicated JmDNS against mDNSResponder — switch to `nativeDnssd(bind)` or share an existing JmDNS | | ||
| | `UnsupportedOperationException: Native DNS-SD` | No JNA on classpath, or 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 / too many interfaces | Prefer `nativeDnssd(localAddress)` instead of all-interfaces `nativeDnssd()` | | ||
|
|
||
| ## 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,122 @@ | ||
| 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>Callers (e.g. openHAB) should choose the advertiser explicitly: use {@link #jmdns(JmDNS)} to | ||
| * share an existing JmDNS instance, {@link #jmdns(InetAddress)} for a dedicated JmDNS socket, or | ||
| * {@link #nativeDnssd()} / {@link #nativeDnssd(InetAddress)} for Bonjour without binding UDP 5353. | ||
| * Do not rely on HAP-Java to pick a policy automatically. | ||
| * | ||
| * <pre>{@code | ||
| * // Force JmDNS (e.g. share openHAB's instance): | ||
| * HomekitAdvertiser adv = HomekitAdvertisers.jmdns(sharedJmDNS); | ||
| * | ||
| * // Native DNS-SD on the interface that owns the HAP bind address (SRV host = system default): | ||
| * HomekitAdvertiser adv = HomekitAdvertisers.nativeDnssd(InetAddress.getByName("192.168.0.6")); | ||
| * }</pre> | ||
| * | ||
| * @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 on <strong>all</strong> interfaces ({@code interfaceIndex = | ||
| * 0}, default host). Prefer {@link #nativeDnssd(InetAddress)} when the HAP TCP listener is bound | ||
| * to a single LAN address. | ||
| * | ||
| * <p>Requires the optional JNA dependency on the classpath. | ||
| * | ||
| * @return a native DNS-SD advertiser | ||
| * @throws UnsupportedOperationException if JNA or 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(); | ||
| } | ||
|
|
||
| /** | ||
| * Native DNS-SD / Bonjour advertiser scoped to the interface that owns {@code localAddress}. | ||
| * Registration uses that interface index when resolvable; SRV host stays {@code null} (system | ||
| * hostname). Passing an IP literal as DNS-SD host is incorrect — it yields an SRV target with no | ||
| * A record and breaks Home pairing. | ||
| * | ||
| * @param localAddress LAN address the HAP TCP server listens on | ||
| * @return a native DNS-SD advertiser | ||
| * @throws UnsupportedOperationException if JNA or the native library cannot be loaded | ||
| */ | ||
| public static HomekitAdvertiser nativeDnssd(InetAddress localAddress) { | ||
| if (!DnssdHomekitAdvertiser.isSupported()) { | ||
| throw new UnsupportedOperationException( | ||
| "Native DNS-SD (dns_sd / Bonjour) is not available on this platform/JVM"); | ||
| } | ||
| return new DnssdHomekitAdvertiser(localAddress); | ||
| } | ||
|
|
||
| /** | ||
| * Convenience: prefer native DNS-SD when the library loads, otherwise JmDNS on {@code | ||
| * localAddress}. | ||
| * | ||
| * <p><strong>Applications should prefer an explicit {@link #jmdns(JmDNS)} / {@link | ||
| * #nativeDnssd(InetAddress)} choice</strong> (policy belongs in openHAB / the integrator). This | ||
| * helper only probes whether the native library <em>loads</em>; it does not verify that | ||
| * registration will succeed on the network. | ||
| * | ||
| * @param localAddress address used for native scoping or JmDNS fallback | ||
| * @return a native DNS-SD advertiser when the library loads, otherwise a JmDNS-backed advertiser | ||
| * @throws IOException if native DNS-SD is unavailable and JmDNS cannot be created | ||
| * @deprecated Prefer {@link #jmdns(JmDNS)}, {@link #jmdns(InetAddress)}, or {@link | ||
| * #nativeDnssd(InetAddress)} so the application chooses the advertiser. | ||
| */ | ||
| @Deprecated | ||
| public static HomekitAdvertiser auto(InetAddress localAddress) throws IOException { | ||
| if (DnssdHomekitAdvertiser.isSupported()) { | ||
| return new DnssdHomekitAdvertiser(localAddress); | ||
| } | ||
| 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 constructed on this JVM/OS. | ||
| * | ||
| * <p>Only checks that JNA and the platform DNS-SD library <em>load</em>. It does not register a | ||
| * service or prove mDNS is reachable on the LAN. | ||
| * | ||
| * @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