Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# HAP-Java 2.0.10
* Add optional Mute characteristic to Doorbell service
* Add pluggable `HomekitAdvertiser` with native DNS-SD / Bonjour implementation (`DnssdHomekitAdvertiser`) so macOS can advertise `_hap._tcp` without binding UDP 5353 [#212](https://github.com/hap-java/HAP-Java/issues/212) (see [docs/NATIVE_DNSSD.md](docs/NATIVE_DNSSD.md))
* Extract shared HAP TXT helpers (`HapDnsTxt`)
* Add optional runtime dependency on JNA (`net.java.dev.jna:jna`) for native `DNSServiceRegister` (macOS `libSystem` / Avahi `libdns_sd`); JmDNS path unchanged when native DNS-SD is not used

# HAP-Java 2.0.9
* Fix Set Duration and Remaining Duration characteristics to advertise format `uint32` instead of `int` [#202](https://github.com/hap-java/HAP-Java/issues/202)
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ Include HAP-Java in your project using maven:
After that, check out the [Sample](https://github.com/hap-java/HAP-Java/tree/sample) and
read the [Javadoc](https://hap-java.github.io/HAP-Java/apidocs/index.html)

Native DNS-SD / Bonjour (macOS)
=========

JmDNS binds UDP **5353**. On macOS that port is owned by `mDNSResponder`, so dedicated JmDNS advertising fails with “Address already in use” and Home never sees `_hap._tcp`.

This build adds a native advertiser that registers through Bonjour without binding 5353:

```java
import io.github.hapjava.server.HomekitAdvertisers;
import io.github.hapjava.server.impl.HomekitServer;

HomekitServer server =
new HomekitServer(bindAddress, 9124, HomekitAdvertisers.nativeDnssd());
```

Or `HomekitAdvertisers.auto(bindAddress)` to prefer native DNS-SD when available. Full details: [docs/NATIVE_DNSSD.md](docs/NATIVE_DNSSD.md).

Supported HomeKit Accessories
=========

Expand Down
124 changes: 124 additions & 0 deletions docs/NATIVE_DNSSD.md
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.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@
<version>3.5.6</version>
</dependency>

<!-- Native Bonjour / DNS-SD (macOS libSystem, Linux libdns_sd) -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.14.0</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/io/github/hapjava/server/HomekitAdvertiser.java
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;
}
87 changes: 87 additions & 0 deletions src/main/java/io/github/hapjava/server/HomekitAdvertisers.java
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);
}

/**
* 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();
}
}
5 changes: 3 additions & 2 deletions src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.hapjava.accessories.Bridge;
import io.github.hapjava.accessories.HomekitAccessory;
import io.github.hapjava.server.HomekitAccessoryCategories;
import io.github.hapjava.server.HomekitAdvertiser;
import io.github.hapjava.server.HomekitAuthInfo;
import io.github.hapjava.server.HomekitWebHandler;
import io.github.hapjava.server.impl.connections.HomekitClientConnectionFactoryImpl;
Expand All @@ -27,7 +28,7 @@ public class HomekitRoot {

private static final Logger logger = LoggerFactory.getLogger(HomekitRoot.class);
private static final int DEFAULT_ACCESSORY_CATEGORY = HomekitAccessoryCategories.OTHER;
private final JmdnsHomekitAdvertiser advertiser;
private final HomekitAdvertiser advertiser;
private final HomekitWebHandler webHandler;
private final HomekitAuthInfo authInfo;
private final String label;
Expand Down Expand Up @@ -60,7 +61,7 @@ public class HomekitRoot {
int category,
HomekitWebHandler webHandler,
HomekitAuthInfo authInfo,
JmdnsHomekitAdvertiser advertiser)
HomekitAdvertiser advertiser)
throws IOException {
this.advertiser = advertiser;
this.webHandler = webHandler;
Expand Down
Loading