homebridge-bluos 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.3](https://github.com/tbaur/homebridge-bluos/compare/v2.0.2...v2.0.3) (2026-09-13)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * skip public and loopback addresses in mDNS discovery ([#46](https://github.com/tbaur/homebridge-bluos/issues/46)) ([9010b86](https://github.com/tbaur/homebridge-bluos/commit/9010b861a95f8d11e450c7c3d877f1f243c2f01d))
9
+
3
10
  ## [2.0.2](https://github.com/tbaur/homebridge-bluos/compare/v2.0.1...v2.0.2) (2026-09-10)
4
11
 
5
12
 
package/README.md CHANGED
@@ -56,7 +56,7 @@ Nothing needs enabling on the player, because the BluOS LAN API is always on. A
56
56
 
57
57
  ### 3. Configure
58
58
 
59
- **Homebridge UI** (recommended): open the plugin settings and press **Discover Players**. Every zone that answers is listed. Tick the players and accessories you want, then press the Homebridge Save button.
59
+ **Homebridge UI** (recommended): open the plugin settings and press **Discover Players**. Every zone that answers on a local IPv4 address is listed. Public and loopback advertisements are skipped. Tick the players and accessories you want, then press the Homebridge Save button.
60
60
 
61
61
  Or in `config.json`:
62
62
 
package/SECURITY.md CHANGED
@@ -16,8 +16,8 @@ Do not open a public issue. Use GitHub's [private vulnerability reporting](https
16
16
  - **Input validation:** Config is checked at startup. A missing or non-list `devices` value disables the platform without unregistering accessories. A bad player or preset is skipped. Hosts, ports and timeouts are rejected or clamped.
17
17
  - **Log safety:** Values written to logs have control characters stripped and are length-limited.
18
18
  - **Bounded I/O:** Connect and total timeouts on every request, responses capped at 128 KiB, and XML parsed with size, depth and element caps.
19
- - **Discovery:** mDNS errors are caught so they cannot take Homebridge down. Browse results are capped.
20
- - **Settings probe:** Accepts only a private or local address and a documented BluOS port.
19
+ - **Discovery:** mDNS errors are caught so they cannot take Homebridge down. Browse results are capped. Advertised addresses are verified only when they are local (RFC 1918, CGNAT/Tailscale, link-local). Public IPv4 and loopback are skipped, so an advertisement cannot make Homebridge open `/SyncStatus` on itself or on the public internet. Players already in configuration are unchanged.
20
+ - **Settings probe:** Accepts only a private or local address and a documented BluOS port. Loopback is allowed here because an administrator asked for that probe.
21
21
  - **Dependencies:** CI runs `npm audit` on the runtime tree and OSV-Scanner on the full tree.
22
22
 
23
23
  ## Best Practices for Users
@@ -112,10 +112,12 @@ export declare class BluOSDiscovery {
112
112
  /**
113
113
  * Turn service instances into addressable endpoints.
114
114
  *
115
- * A zone is only usable once its SRV record (for the port) and an IPv4 address
116
- * are both known. The address comes from an A record when one was offered, and
117
- * otherwise from the responder's own source address, which for a player
118
- * advertising its own service is the same machine.
115
+ * A zone is only usable once its SRV record (for the port) and a local IPv4
116
+ * address are both known (RFC 1918, CGNAT / Tailscale, link-local). Public
117
+ * IPv4 and loopback are ignored, so an advertisement cannot point verify at
118
+ * Homebridge or at an off-network host. The address comes from an A record
119
+ * when one was offered, and otherwise from the responder's own source
120
+ * address, which for a player advertising its own service is the same machine.
119
121
  */
120
122
  private toEndpoints;
121
123
  /** Collect mDNS records for the configured window. */
@@ -35,6 +35,7 @@ const node_os_1 = __importDefault(require("node:os"));
35
35
  const settings_1 = require("../settings");
36
36
  const errors_1 = require("../utils/errors");
37
37
  const timing_1 = require("../utils/timing");
38
+ const validators_1 = require("../utils/validators");
38
39
  const identity_1 = require("./identity");
39
40
  /** Re-query schedule, in milliseconds, to survive dropped multicast packets. */
40
41
  const QUERY_SCHEDULE_MS = [0, 400, 1_200];
@@ -202,10 +203,12 @@ class BluOSDiscovery {
202
203
  /**
203
204
  * Turn service instances into addressable endpoints.
204
205
  *
205
- * A zone is only usable once its SRV record (for the port) and an IPv4 address
206
- * are both known. The address comes from an A record when one was offered, and
207
- * otherwise from the responder's own source address, which for a player
208
- * advertising its own service is the same machine.
206
+ * A zone is only usable once its SRV record (for the port) and a local IPv4
207
+ * address are both known (RFC 1918, CGNAT / Tailscale, link-local). Public
208
+ * IPv4 and loopback are ignored, so an advertisement cannot point verify at
209
+ * Homebridge or at an off-network host. The address comes from an A record
210
+ * when one was offered, and otherwise from the responder's own source
211
+ * address, which for a player advertising its own service is the same machine.
209
212
  */
210
213
  toEndpoints(candidates) {
211
214
  const seen = new Set();
@@ -217,6 +220,13 @@ class BluOSDiscovery {
217
220
  this.log.debug(`discovery: no IPv4 address for ${candidate.instance} (target ${candidate.target})`);
218
221
  continue;
219
222
  }
223
+ if (!(0, validators_1.isDiscoveryHost)(host)) {
224
+ const reason = (0, validators_1.isLoopbackIpv4)(host)
225
+ ? 'loopback is not accepted from mDNS'
226
+ : 'not a local address';
227
+ this.log.debug(`discovery: skipping ${host} (${reason})`);
228
+ continue;
229
+ }
220
230
  const key = (0, identity_1.formatEndpoint)(host, candidate.port);
221
231
  if (seen.has(key)) {
222
232
  continue;
@@ -85,6 +85,17 @@ export declare function isNonPrivateIpv4(value: string): boolean;
85
85
  * hostnames are accepted.
86
86
  */
87
87
  export declare function isProbeableHost(value: unknown): value is string;
88
+ /**
89
+ * True for an address discovery may dial after an unsolicited mDNS advertisement.
90
+ *
91
+ * Narrower than {@link isProbeableHost}. The settings-page probe is an
92
+ * administrator asking this host to open a connection, so loopback is allowed
93
+ * there. An advertisement on the LAN is not a request, and a packet that names
94
+ * 127.0.0.1 would make Homebridge talk to itself.
95
+ */
96
+ export declare function isDiscoveryHost(value: unknown): value is string;
97
+ /** True for IPv4 loopback (first octet 127). `isProbeableHost` treats this as local; discovery does not. */
98
+ export declare function isLoopbackIpv4(value: string): boolean;
88
99
  /** Clamp the discovery window into the supported range. */
89
100
  export declare function resolveDiscoveryTimeoutSec(value: unknown, warnings?: string[]): number;
90
101
  /**
@@ -25,6 +25,8 @@ exports.isIpv4 = isIpv4;
25
25
  exports.isValidHost = isValidHost;
26
26
  exports.isNonPrivateIpv4 = isNonPrivateIpv4;
27
27
  exports.isProbeableHost = isProbeableHost;
28
+ exports.isDiscoveryHost = isDiscoveryHost;
29
+ exports.isLoopbackIpv4 = isLoopbackIpv4;
28
30
  exports.resolveDiscoveryTimeoutSec = resolveDiscoveryTimeoutSec;
29
31
  exports.resolveSliderService = resolveSliderService;
30
32
  exports.validateConfig = validateConfig;
@@ -169,6 +171,28 @@ function isProbeableHost(value) {
169
171
  }
170
172
  return PROBE_LOCAL_SUFFIXES.some((suffix) => lower.endsWith(suffix) && lower.length > suffix.length);
171
173
  }
174
+ /**
175
+ * True for an address discovery may dial after an unsolicited mDNS advertisement.
176
+ *
177
+ * Narrower than {@link isProbeableHost}. The settings-page probe is an
178
+ * administrator asking this host to open a connection, so loopback is allowed
179
+ * there. An advertisement on the LAN is not a request, and a packet that names
180
+ * 127.0.0.1 would make Homebridge talk to itself.
181
+ */
182
+ function isDiscoveryHost(value) {
183
+ if (!isProbeableHost(value)) {
184
+ return false;
185
+ }
186
+ return !isLoopbackIpv4(value.trim());
187
+ }
188
+ /** True for IPv4 loopback (first octet 127). `isProbeableHost` treats this as local; discovery does not. */
189
+ function isLoopbackIpv4(value) {
190
+ if (!isIpv4(value)) {
191
+ return false;
192
+ }
193
+ const [firstOctet = 0] = value.split('.').map(Number);
194
+ return firstOctet === 127;
195
+ }
172
196
  /** Clamp the discovery window into the supported range. */
173
197
  function resolveDiscoveryTimeoutSec(value, warnings) {
174
198
  if (value === undefined) {
package/docs/FEATURES.md CHANGED
@@ -42,7 +42,7 @@ A checklist of what is built. The plugin aims to cover everything about a BluOS
42
42
  - ✅ An unusable configuration disables the platform and keeps every accessory registered. Nothing is deleted
43
43
  - ✅ Per-device validation: one bad entry is skipped with a warning instead of stopping the rest
44
44
  - ✅ Size-, depth-, element- and attribute-capped XML parsing, sanitised log output, and a length cap on any identity a player reports for itself
45
- - ✅ Bounded discovery: the records kept from a browse, the candidates verified from it and the verifications in flight are all capped
45
+ - ✅ Bounded discovery: the records kept from a browse, the candidates verified from it and the verifications in flight are all capped. Public IPv4 and loopback advertisements are not verified
46
46
  - ✅ Clean shutdown: poll loops, backoff delays and mDNS browses are cancelled, not left to run out
47
47
  - ✅ A cached accessory that the plugin cannot drive reports No Response and says what to do about it. It never shows a stale value forever
48
48
  - ✅ Custom Homebridge UI settings page, plus a plain `config.schema.json` form
package/docs/PROTOCOL.md CHANGED
@@ -259,7 +259,7 @@ mDNS, browsing two service types:
259
259
  | `_musc._tcp.local` | Primary players |
260
260
  | `_musp._tcp.local` | Secondary zones of a multi-zone chassis (spec appendix §13.1, LSDP class `0x0003`) |
261
261
 
262
- A zone is usable once its `SRV` record (for the port) and an IPv4 address are both known; the address comes from an `A` record when one is offered and otherwise from the responder's own source address. `TXT` records carry `model`, `version`, `mac` and `zs`, but secondary zones **omit `mac`**. That is why identity is always confirmed by reading `/SyncStatus`, and never trusted from the advertisement alone.
262
+ A zone is usable once its `SRV` record (for the port) and a **local** IPv4 address are both known; the address comes from an `A` record when one is offered and otherwise from the responder's own source address. Public IPv4 and loopback advertisements are ignored, so a packet on the LAN cannot steer verify at Homebridge itself or at an off-network host. `TXT` records carry `model`, `version`, `mac` and `zs`, but secondary zones **omit `mac`**. That is why identity is always confirmed by reading `/SyncStatus`, and never trusted from the advertisement alone.
263
263
 
264
264
  **LSDP** (UDP 11430) is documented as an alternative and was tried first: it failed repeatedly against this fleet, while mDNS answered reliably. mDNS is therefore the only discovery path, with manual address entry as the fallback for networks that filter multicast.
265
265
 
@@ -60,7 +60,7 @@ Name it with `options.rebootAllName`. It is the one accessory with no room of it
60
60
 
61
61
  ## Reliability in detail
62
62
 
63
- **Discovery in the settings page.** Finds zones over mDNS and writes the configuration for you, including the stable identity the platform will look for. Manual address entry covers networks where multicast is filtered.
63
+ **Discovery in the settings page.** Finds zones over mDNS and writes the configuration for you, including the stable identity the platform will look for. Only local IPv4 advertisements are verified (private, CGNAT/Tailscale, link-local). Public and loopback addresses are skipped. Manual address entry covers networks where multicast is filtered; the settings probe still allows loopback because you typed it.
64
64
 
65
65
  **Long-polling, not polling.** `/SyncStatus?timeout=100` with an `etag`, so a change made on the front panel, the remote or the BluOS app reaches HomeKit in about a second, without hammering the player. One poll loop per zone, because the etag is per zone and not per chassis.
66
66
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-bluos",
3
3
  "displayName": "Homebridge BluOS",
4
- "version": "2.0.2",
4
+ "version": "2.0.3",
5
5
  "description": "Homebridge plugin for BluOS players — per-zone volume, mute, volume-preset and battery accessories over the LAN Custom Integration API. Verified on NAD and Bluesound hardware",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",