Skip to content

Commit 465274d

Browse files
kgoderiscursoragent
andcommitted
Fix nativeDnssd(InetAddress) pairing hang from IP SRV host.
Keep DNS-SD host null so mDNSResponder publishes the system hostname with A/AAAA; only scope registration by interface index. A literal IP as host became an SRV target with no address record. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent fbc2961 commit 465274d

5 files changed

Lines changed: 32 additions & 20 deletions

File tree

‎CHANGES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* 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))
44
* Extract shared HAP TXT helpers (`HapDnsTxt`) with stable key order; JmDNS and native advertisers publish the same property set
55
* JNA (`net.java.dev.jna:jna`) is **optional** for `nativeDnssd()`; JmDNS-only consumers do not need it. Applications should choose `jmdns(...)` or `nativeDnssd(InetAddress)` explicitly (`auto(...)` is deprecated)
6+
* `nativeDnssd(InetAddress)` scopes DNS-SD by interface index only and keeps SRV `host` null (system hostname). Passing an IP literal as host broke pairing (SRV target with no A record)
67

78
# HAP-Java 2.0.9
89
* Fix Set Duration and Remaining Duration characteristics to advertise format `uint32` instead of `int` [#202](https://github.com/hap-java/HAP-Java/issues/202)

‎docs/NATIVE_DNSSD.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import java.net.InetAddress;
4040

4141
InetAddress bind = InetAddress.getByName("192.168.0.6");
4242

43-
// Prefer scoping native registration to the HAP bind address (interface + SRV host):
43+
// Prefer scoping native registration to the HAP bind interface (SRV host stays system default):
4444
HomekitServer server =
4545
new HomekitServer(bind, 9124, HomekitAdvertisers.nativeDnssd(bind));
4646

@@ -91,7 +91,7 @@ setConfigurationIndex(int)
9191
| `jmdns(JmDNS)` | Wrap an existing JmDNS (force JmDNS / share openHAB’s instance) |
9292
| `jmdns(InetAddress)` | Create dedicated JmDNS on that address |
9393
| `nativeDnssd()` | Native Bonjour on **all** interfaces (`interfaceIndex = 0`, default host) |
94-
| `nativeDnssd(InetAddress)` | Native Bonjour scoped to that address’s interface index + SRV host |
94+
| `nativeDnssd(InetAddress)` | Native Bonjour on that address’s interface index; SRV host = system default (`null`) |
9595
| `isNativeDnssdSupported()` | Whether JNA + dns_sd **load** (not a live register probe) |
9696
| `auto(InetAddress)` | **Deprecated** — prefer an explicit `jmdns` / `nativeDnssd` choice |
9797

@@ -108,7 +108,7 @@ When an advertiser is supplied, `createBridge` / `createStandaloneAccessory` use
108108

109109
* **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`.
110110
* `DNSServiceRegister` receives the TCP port in **network byte order**.
111-
* Default `nativeDnssd()` uses `interfaceIndex = 0` (all interfaces) and `host = null` (system hostname). Prefer `nativeDnssd(InetAddress)` when the HAP TCP listener is bound to one LAN address.
111+
* 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”).
112112
* 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.
113113
* A daemon thread runs `DNSServiceProcessResult` until `DNSServiceRefDeallocate` on `stop()` / re-register.
114114
* Re-registration (discoverability, MAC, `c#`) deallocates and registers again — same pattern as the JmDNS advertiser.

‎src/main/java/io/github/hapjava/server/HomekitAdvertisers.java‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* // Force JmDNS (e.g. share openHAB's instance):
1919
* HomekitAdvertiser adv = HomekitAdvertisers.jmdns(sharedJmDNS);
2020
*
21-
* // Native DNS-SD limited to one LAN address / interface:
21+
* // Native DNS-SD on the interface that owns the HAP bind address (SRV host = system default):
2222
* HomekitAdvertiser adv = HomekitAdvertisers.nativeDnssd(InetAddress.getByName("192.168.0.6"));
2323
* }</pre>
2424
*
@@ -68,8 +68,10 @@ public static HomekitAdvertiser nativeDnssd() {
6868
}
6969

7070
/**
71-
* Native DNS-SD / Bonjour advertiser scoped to {@code localAddress}: SRV host is that address and
72-
* registration uses the matching network interface index when resolvable.
71+
* Native DNS-SD / Bonjour advertiser scoped to the interface that owns {@code localAddress}.
72+
* Registration uses that interface index when resolvable; SRV host stays {@code null} (system
73+
* hostname). Passing an IP literal as DNS-SD host is incorrect — it yields an SRV target with no
74+
* A record and breaks Home pairing.
7375
*
7476
* @param localAddress LAN address the HAP TCP server listens on
7577
* @return a native DNS-SD advertiser

‎src/main/java/io/github/hapjava/server/impl/dnssd/DnssdHomekitAdvertiser.java‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
*
2323
* <p>Uses JNA to call {@code DNSServiceRegister} / {@code DNSServiceRefDeallocate}.
2424
*
25-
* <p>Pass an {@link InetAddress} to scope registration to one LAN interface/host; the no-arg
26-
* constructor registers on all interfaces ({@code interfaceIndex = 0}) with the system default
27-
* hostname ({@code host = null}).
25+
* <p>Pass an {@link InetAddress} to scope registration to one LAN <em>interface</em>; SRV host
26+
* stays {@code null} so mDNSResponder advertises the system hostname (with A/AAAA). Passing a raw
27+
* IP as DNS-SD {@code host} breaks pairing — the daemon does not synthesize an A record for that
28+
* name. The no-arg constructor registers on all interfaces ({@code interfaceIndex = 0}).
2829
*
2930
* @author Karel Goderis
3031
*/
@@ -62,21 +63,25 @@ public DnssdHomekitAdvertiser() {
6263
}
6364

6465
/**
65-
* Scope registration to {@code localAddress}: SRV host is that address's host string, and the
66-
* DNS-SD interface index is taken from the matching {@link NetworkInterface} when available.
66+
* Scope registration to the network interface that owns {@code localAddress}. SRV {@code host} is
67+
* left {@code null} (system default hostname + A/AAAA via mDNSResponder).
68+
*
69+
* <p>Do not pass {@code localAddress.getHostAddress()} as DNS-SD host: a literal IP becomes an
70+
* SRV target name with no A record, and Home pairing hangs after accepting the QR code.
6771
*
6872
* @param localAddress LAN address the HAP TCP listener uses
6973
*/
7074
public DnssdHomekitAdvertiser(InetAddress localAddress) {
71-
this(resolveInterfaceIndex(localAddress), resolveHost(localAddress));
75+
this(resolveInterfaceIndex(localAddress), null);
7276
}
7377

7478
/**
7579
* Full control over DNS-SD {@code interfaceIndex} and {@code host}.
7680
*
7781
* @param interfaceIndex {@link #INTERFACE_INDEX_ANY} (0) for all interfaces, or a platform
7882
* interface index from {@link NetworkInterface#getIndex()}
79-
* @param host SRV target host (IP or DNS name), or {@code null} for the system default hostname
83+
* @param host SRV target <strong>DNS hostname</strong>, or {@code null} for the system default.
84+
* Must not be a raw IP literal — DNS-SD will not register an A/AAAA for it.
8085
*/
8186
public DnssdHomekitAdvertiser(int interfaceIndex, String host) {
8287
if (!SUPPORTED) {
@@ -125,11 +130,6 @@ private static boolean detectSupport() {
125130
}
126131
}
127132

128-
private static String resolveHost(InetAddress localAddress) {
129-
Objects.requireNonNull(localAddress, "localAddress");
130-
return localAddress.getHostAddress();
131-
}
132-
133133
private static int resolveInterfaceIndex(InetAddress localAddress) {
134134
Objects.requireNonNull(localAddress, "localAddress");
135135
try {

‎src/test/java/io/github/hapjava/server/impl/dnssd/DnssdHomekitAdvertiserTest.java‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ public void advertiseRegistersHapService() throws Exception {
9393

9494
@Test
9595
public void advertisePassesConfiguredInterfaceIndexAndHost() throws Exception {
96-
subject = new DnssdHomekitAdvertiser(dnsSd, 4, "192.168.0.6");
96+
// Advanced ctor may pass a real DNS hostname; raw IPs must not be used as DNS-SD host.
97+
subject = new DnssdHomekitAdvertiser(dnsSd, 4, "bridge.local");
9798
when(dnsSd.DNSServiceRegister(
9899
any(PointerByReference.class),
99100
anyInt(),
@@ -129,7 +130,15 @@ public void advertisePassesConfiguredInterfaceIndexAndHost() throws Exception {
129130
any());
130131

131132
assertThat(ifIndex.getValue()).isEqualTo(4);
132-
assertThat(host.getValue()).isEqualTo("192.168.0.6");
133+
assertThat(host.getValue()).isEqualTo("bridge.local");
134+
}
135+
136+
@Test
137+
public void inetAddressConstructorLeavesHostNull() {
138+
// Package-visible mock ctor mirrors InetAddress behaviour: interface scoped, host=null.
139+
subject = new DnssdHomekitAdvertiser(dnsSd, 7, null);
140+
assertThat(subject.getInterfaceIndex()).isEqualTo(7);
141+
assertThat(subject.getHost()).isNull();
133142
}
134143

135144
@Test

0 commit comments

Comments
 (0)