matterbridge 3.4.5-dev-20251224-e00e572 → 3.4.5-dev-20251224-1bb63cb

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
@@ -34,10 +34,14 @@ Advantages:
34
34
 
35
35
  - [DevContainer]: Refactored Dev Container setup. The Matterbridge instance can now be paired on native Linux hosts or WSL 2 with Docker engine CLI integration. On Docker Desktop on Windows or macOS is not possible cause Docker Desktop runs inside a VM and not directly on the host so mDNS is not supported.
36
36
  - [DevContainer]: Since is now possible to pair from Dev Container, named volumes have been added to persist storage and plugins across rebuilds.
37
+ - [mb_mdns]: Add query and advertise interval. Improved help for easy mDNS testing.
38
+ - [mb_mdns]: Advertise full DNS-SD record set (PTR/SRV/TXT/A/AAAA) for matterbridge.\_http.\_tcp.local on port 8283.
39
+ - [mdns]: Added TXT/SRV/A/AAAA encoders into the Mdns class.
37
40
 
38
41
  ### Changed
39
42
 
40
43
  - [package]: Updated dependencies.
44
+ - [mdns]: Mdns.sendResponse now accepts an array and sends multi-answer responses.
41
45
 
42
46
  ### Fixed
43
47
 
@@ -1,37 +1,49 @@
1
- import { getParameter, getStringArrayParameter, hasParameter } from '../utils/commandLine.js';
1
+ import { getIntParameter, getParameter, getStringArrayParameter, hasParameter } from '../utils/commandLine.js';
2
+ import { getIpv4InterfaceAddress, getIpv6InterfaceAddress } from '../utils/network.js';
2
3
  import { MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT } from './multicast.js';
3
4
  import { Mdns } from './mdns.js';
4
5
  {
5
6
  if (hasParameter('h') || hasParameter('help')) {
6
- console.log(`Usage: mb_mdns [options]
7
+ console.log(`Copyright (c) Matterbridge. All rights reserved.\n`);
8
+ console.log(`Usage: mb_mdns [options...]
9
+
10
+ If no command line is provided, mb_mdns shows all incoming mDNS records.
7
11
 
8
12
  Options:
9
13
  -h, --help Show this help message and exit.
10
14
  --interfaceName <name> Network interface name to bind to (default all interfaces).
11
15
  --ipv4InterfaceAddress <address> IPv4 address of the network interface to bind to (default: 0.0.0.0).
12
16
  --ipv6InterfaceAddress <address> IPv6 address of the network interface to bind to (default: ::).
13
- --outgoingIpv4InterfaceAddress <address> Outgoing IPv4 address (default first external address).
14
- --outgoingIpv6InterfaceAddress <address> Outgoing IPv6 address (default first external address).
15
- --advertise Enable mDNS advertisement (default: disabled).
16
- --query Enable mDNS query (default: disabled).
17
- --filter <string> Filter string to match in the mDNS record name (can be repeated).
17
+ --outgoingIpv4InterfaceAddress <address> Outgoing IPv4 address of the network interface (default first external address).
18
+ --outgoingIpv6InterfaceAddress <address> Outgoing IPv6 address of the network interface (default first external address).
19
+ --advertise <interval> Enable matterbridge mDNS advertisement each ms (default interval: 10000ms).
20
+ --query <interval> Enable common mDNS services query each ms (default interval: 10000ms).
21
+ --filter <string...> Filter strings to match in the mDNS record name (default: no filter).
18
22
  -v, --verbose Enable verbose logging (default: disabled).
19
23
 
20
24
  Examples:
21
- # List Matter device commissioner service records only on eth0 interface
25
+ # Listen for Matter device commissioner service records only on eth0 interface
22
26
  mb_mdns --interfaceName eth0 --filter _matterc._udp
23
27
 
24
- # List Matter device service records only on eth0 interface
28
+ # Listen for Matter device discovery service records only on eth0 interface
25
29
  mb_mdns --interfaceName eth0 --filter _matter._tcp
26
30
 
27
- # List both Matter commissioner and device service records only on eth0 interface
28
- mb_mdns --interfaceName eth0 --filter _matterc._udp _matter._tcp
31
+ # Listen for Matter commissioner and discovery service records on all interfaces
32
+ mb_mdns --filter _matterc._udp _matter._tcp
29
33
 
30
34
  # Query for mDNS devices every 10s on a specific interface
31
35
  mb_mdns --interfaceName eth0 --query
36
+
37
+ # Advertise matterbridge._http._tcp.local every 5s with filter
38
+ mb_mdns --advertise 5000 --filter matterbridge._http._tcp.local
39
+
40
+ # Query each 5s and listen for matterbridge._http._tcp.local service records
41
+ mb_mdns --query 5000 --filter matterbridge._http._tcp.local
42
+
32
43
  `);
33
44
  process.exit(0);
34
45
  }
46
+ const { default: pkg } = await import('../../package.json', { with: { type: 'json' } });
35
47
  const mdnsIpv4 = new Mdns('mDNS Server udp4', MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_PORT, 'udp4', true, getParameter('interfaceName'), getParameter('ipv4InterfaceAddress') || '0.0.0.0', getParameter('outgoingIpv4InterfaceAddress'));
36
48
  const mdnsIpv6 = new Mdns('mDNS Server udp6', MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT, 'udp6', true, getParameter('interfaceName'), getParameter('ipv6InterfaceAddress') || '::', getParameter('outgoingIpv6InterfaceAddress'));
37
49
  if (hasParameter('v') || hasParameter('verbose')) {
@@ -56,7 +68,9 @@ Examples:
56
68
  process.exit(0);
57
69
  }
58
70
  const queryUdp4 = () => {
71
+ mdnsIpv4.log.info('Sending mDNS query for services...');
59
72
  mdnsIpv4.sendQuery([
73
+ { name: '_matterc._udp.local', type: 12, class: 1, unicastResponse: false },
60
74
  { name: '_matter._tcp.local', type: 12, class: 1, unicastResponse: false },
61
75
  { name: '_shelly._tcp.local', type: 12, class: 1, unicastResponse: false },
62
76
  { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: false },
@@ -64,11 +78,44 @@ Examples:
64
78
  ]);
65
79
  };
66
80
  const advertiseUdp4 = () => {
67
- const ptrRdata = mdnsIpv4.encodeDnsName('matterbridge._http._tcp.local');
68
- mdnsIpv4.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
81
+ mdnsIpv4.log.info('Sending mDNS advertisement for matterbridge service...');
82
+ const serviceType = '_http._tcp.local';
83
+ const instanceName = 'matterbridge._http._tcp.local';
84
+ const hostName = 'matterbridge.local';
85
+ const port = 8283;
86
+ const ttl = 120;
87
+ const ptrInstanceRdata = mdnsIpv4.encodeDnsName(instanceName);
88
+ const ptrServiceTypeRdata = mdnsIpv4.encodeDnsName(serviceType);
89
+ const srvRdata = mdnsIpv4.encodeSrvRdata(0, 0, port, hostName);
90
+ const txtRdata = mdnsIpv4.encodeTxtRdata([`version=${pkg.version}`, 'path=/']);
91
+ const answers = [
92
+ { name: '_services._dns-sd._udp.local', rtype: 12, rclass: 1, ttl, rdata: ptrServiceTypeRdata },
93
+ { name: serviceType, rtype: 12, rclass: 1, ttl, rdata: ptrInstanceRdata },
94
+ { name: instanceName, rtype: 33, rclass: 1 | 32768, ttl, rdata: srvRdata },
95
+ { name: instanceName, rtype: 16, rclass: 1 | 32768, ttl, rdata: txtRdata },
96
+ ];
97
+ try {
98
+ const ipv4 = getIpv4InterfaceAddress();
99
+ if (ipv4 && ipv4 !== '0.0.0.0') {
100
+ answers.push({ name: hostName, rtype: 1, rclass: 1 | 32768, ttl, rdata: mdnsIpv4.encodeA(ipv4) });
101
+ }
102
+ }
103
+ catch {
104
+ }
105
+ try {
106
+ const ipv6 = getIpv6InterfaceAddress();
107
+ if (ipv6 && ipv6 !== '::') {
108
+ answers.push({ name: hostName, rtype: 28, rclass: 1 | 32768, ttl, rdata: mdnsIpv4.encodeAAAA(ipv6) });
109
+ }
110
+ }
111
+ catch {
112
+ }
113
+ mdnsIpv4.sendResponse(answers);
69
114
  };
70
115
  const queryUdp6 = () => {
116
+ mdnsIpv6.log.info('Sending mDNS query for services...');
71
117
  mdnsIpv6.sendQuery([
118
+ { name: '_matterc._udp.local', type: 12, class: 1, unicastResponse: true },
72
119
  { name: '_matter._tcp.local', type: 12, class: 1, unicastResponse: true },
73
120
  { name: '_shelly._tcp.local', type: 12, class: 1, unicastResponse: true },
74
121
  { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: true },
@@ -76,8 +123,39 @@ Examples:
76
123
  ]);
77
124
  };
78
125
  const advertiseUdp6 = () => {
79
- const ptrRdata = mdnsIpv6.encodeDnsName('matterbridge._http._tcp.local');
80
- mdnsIpv6.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
126
+ mdnsIpv6.log.info('Sending mDNS advertisement for matterbridge service...');
127
+ const serviceType = '_http._tcp.local';
128
+ const instanceName = 'matterbridge._http._tcp.local';
129
+ const hostName = 'matterbridge.local';
130
+ const port = 8283;
131
+ const ttl = 120;
132
+ const ptrInstanceRdata = mdnsIpv6.encodeDnsName(instanceName);
133
+ const ptrServiceTypeRdata = mdnsIpv6.encodeDnsName(serviceType);
134
+ const srvRdata = mdnsIpv6.encodeSrvRdata(0, 0, port, hostName);
135
+ const txtRdata = mdnsIpv6.encodeTxtRdata([`version=${pkg.version}`, 'path=/']);
136
+ const answers = [
137
+ { name: '_services._dns-sd._udp.local', rtype: 12, rclass: 1, ttl, rdata: ptrServiceTypeRdata },
138
+ { name: serviceType, rtype: 12, rclass: 1, ttl, rdata: ptrInstanceRdata },
139
+ { name: instanceName, rtype: 33, rclass: 1 | 32768, ttl, rdata: srvRdata },
140
+ { name: instanceName, rtype: 16, rclass: 1 | 32768, ttl, rdata: txtRdata },
141
+ ];
142
+ try {
143
+ const ipv4 = getIpv4InterfaceAddress();
144
+ if (ipv4) {
145
+ answers.push({ name: hostName, rtype: 1, rclass: 1 | 32768, ttl, rdata: mdnsIpv6.encodeA(ipv4) });
146
+ }
147
+ }
148
+ catch {
149
+ }
150
+ try {
151
+ const ipv6 = getIpv6InterfaceAddress();
152
+ if (ipv6) {
153
+ answers.push({ name: hostName, rtype: 28, rclass: 1 | 32768, ttl, rdata: mdnsIpv6.encodeAAAA(ipv6) });
154
+ }
155
+ }
156
+ catch {
157
+ }
158
+ mdnsIpv6.sendResponse(answers);
81
159
  };
82
160
  process.on('SIGINT', () => {
83
161
  cleanupAndLogAndExit();
@@ -85,33 +163,33 @@ Examples:
85
163
  mdnsIpv4.start();
86
164
  mdnsIpv4.on('ready', (address) => {
87
165
  mdnsIpv4.log.info(`mdnsIpv4 server ready on ${address.family} ${address.address}:${address.port}`);
88
- if (getParameter('advertise')) {
166
+ if (hasParameter('advertise')) {
89
167
  advertiseUdp4();
90
168
  setInterval(() => {
91
169
  advertiseUdp4();
92
- }, 10000).unref();
170
+ }, getIntParameter('advertise') || 10000).unref();
93
171
  }
94
- if (getParameter('query')) {
172
+ if (hasParameter('query')) {
95
173
  queryUdp4();
96
174
  setInterval(() => {
97
175
  queryUdp4();
98
- }, 10000).unref();
176
+ }, getIntParameter('query') || 10000).unref();
99
177
  }
100
178
  });
101
179
  mdnsIpv6.start();
102
180
  mdnsIpv6.on('ready', (address) => {
103
181
  mdnsIpv6.log.info(`mdnsIpv6 server ready on ${address.family} ${address.address}:${address.port}`);
104
- if (getParameter('advertise')) {
182
+ if (hasParameter('advertise')) {
105
183
  advertiseUdp6();
106
184
  setInterval(() => {
107
185
  advertiseUdp6();
108
- }, 10000).unref();
186
+ }, getIntParameter('advertise') || 10000).unref();
109
187
  }
110
- if (getParameter('query')) {
188
+ if (hasParameter('query')) {
111
189
  queryUdp6();
112
190
  setInterval(() => {
113
191
  queryUdp6();
114
- }, 10000).unref();
192
+ }, getIntParameter('query') || 10000).unref();
115
193
  }
116
194
  });
117
195
  setTimeout(() => {
@@ -271,6 +271,56 @@ export class Mdns extends Multicast {
271
271
  });
272
272
  return Buffer.concat([...buffers, Buffer.from([0])]);
273
273
  }
274
+ encodeTxtRdata(txt) {
275
+ const parts = txt.map((entry) => {
276
+ const value = Buffer.from(entry, 'utf8');
277
+ if (value.length > 255)
278
+ throw new Error(`TXT entry too long: ${entry}`);
279
+ return Buffer.concat([Buffer.from([value.length]), value]);
280
+ });
281
+ return Buffer.concat(parts);
282
+ }
283
+ encodeSrvRdata(priority, weight, port, target) {
284
+ const fixed = Buffer.alloc(6);
285
+ fixed.writeUInt16BE(priority, 0);
286
+ fixed.writeUInt16BE(weight, 2);
287
+ fixed.writeUInt16BE(port, 4);
288
+ return Buffer.concat([fixed, this.encodeDnsName(target)]);
289
+ }
290
+ encodeA(ipv4) {
291
+ const parts = ipv4.split('.').map((p) => Number(p));
292
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
293
+ throw new Error(`Invalid IPv4 address: ${ipv4}`);
294
+ }
295
+ return Buffer.from(parts);
296
+ }
297
+ encodeAAAA(ipv6WithOptionalScope) {
298
+ const ipv6 = ipv6WithOptionalScope.split('%')[0];
299
+ const [left, right] = ipv6.split('::');
300
+ const leftParts = left ? left.split(':').filter(Boolean) : [];
301
+ const rightParts = right ? right.split(':').filter(Boolean) : [];
302
+ if (ipv6.includes('::')) {
303
+ const missing = 8 - (leftParts.length + rightParts.length);
304
+ if (missing < 0)
305
+ throw new Error(`Invalid IPv6 address: ${ipv6WithOptionalScope}`);
306
+ const groups = [...leftParts, ...Array(missing).fill('0'), ...rightParts];
307
+ return Buffer.from(groups.flatMap((g) => {
308
+ const word = parseInt(g || '0', 16);
309
+ if (!Number.isFinite(word) || word < 0 || word > 0xffff)
310
+ throw new Error(`Invalid IPv6 group: ${g}`);
311
+ return [(word >> 8) & 0xff, word & 0xff];
312
+ }));
313
+ }
314
+ const groups = ipv6.split(':');
315
+ if (groups.length !== 8)
316
+ throw new Error(`Invalid IPv6 address: ${ipv6WithOptionalScope}`);
317
+ return Buffer.from(groups.flatMap((g) => {
318
+ const word = parseInt(g || '0', 16);
319
+ if (!Number.isFinite(word) || word < 0 || word > 0xffff)
320
+ throw new Error(`Invalid IPv6 group: ${g}`);
321
+ return [(word >> 8) & 0xff, word & 0xff];
322
+ }));
323
+ }
274
324
  decodeResourceRecord(msg, offset) {
275
325
  const nameResult = this.decodeDnsName(msg, offset);
276
326
  const name = nameResult.name;
@@ -399,29 +449,36 @@ export class Mdns extends Multicast {
399
449
  }
400
450
  });
401
451
  }
402
- sendResponse(name, rtype, rclass, ttl, rdata) {
452
+ sendResponse(answers) {
453
+ if (!Array.isArray(answers) || answers.length === 0) {
454
+ throw new Error('sendResponse requires a non-empty answers array');
455
+ }
403
456
  const header = Buffer.alloc(12);
404
457
  header.writeUInt16BE(0, 0);
405
458
  header.writeUInt16BE(0x8400, 2);
406
459
  header.writeUInt16BE(0, 4);
407
- header.writeUInt16BE(1, 6);
460
+ header.writeUInt16BE(answers.length, 6);
408
461
  header.writeUInt16BE(0, 8);
409
462
  header.writeUInt16BE(0, 10);
410
- const aname = this.encodeDnsName(name);
411
- const answerFixed = Buffer.alloc(10);
412
- answerFixed.writeUInt16BE(rtype, 0);
413
- answerFixed.writeUInt16BE(rclass, 2);
414
- answerFixed.writeUInt32BE(ttl, 4);
415
- answerFixed.writeUInt16BE(rdata.length, 8);
416
- const answer = Buffer.concat([aname, answerFixed, rdata]);
417
- const response = Buffer.concat([header, answer]);
463
+ const answerBuffers = answers.map(({ name, rtype, rclass, ttl, rdata }) => {
464
+ const aname = this.encodeDnsName(name);
465
+ const answerFixed = Buffer.alloc(10);
466
+ answerFixed.writeUInt16BE(rtype, 0);
467
+ answerFixed.writeUInt16BE(rclass, 2);
468
+ answerFixed.writeUInt32BE(ttl, 4);
469
+ answerFixed.writeUInt16BE(rdata.length, 8);
470
+ return Buffer.concat([aname, answerFixed, rdata]);
471
+ });
472
+ const response = Buffer.concat([header, ...answerBuffers]);
418
473
  this.socket.send(response, 0, response.length, this.multicastPort, this.multicastAddress, (error) => {
419
474
  if (error) {
420
- this.log.error(`Dgram mDNS server failed to send response message for ${MAGENTA}${name}${er} type ${MAGENTA}${this.dnsTypeToString(rtype)}${er} class ${MAGENTA}${this.dnsResponseClassToString(rclass)}${er} ttl ${MAGENTA}${ttl}${er}: ${error instanceof Error ? error.message : error}`);
475
+ const items = answers.map((a) => `- name ${MAGENTA}${a.name}${er} type ${MAGENTA}${this.dnsTypeToString(a.rtype)}${er} class ${MAGENTA}${this.dnsResponseClassToString(a.rclass)}${er} ttl ${MAGENTA}${a.ttl}${er}`).join('\n');
476
+ this.log.error(`Dgram mDNS server failed to send response message (${MAGENTA}${answers.length}${er} answers): ${error instanceof Error ? error.message : error}\n${items}`);
421
477
  this.emit('error', error);
422
478
  }
423
479
  else {
424
- this.log.debug(`Dgram mDNS server sent response message for ${MAGENTA}${name}${db} type ${MAGENTA}${this.dnsTypeToString(rtype)}${db} class ${MAGENTA}${this.dnsResponseClassToString(rclass)}${db} ttl ${MAGENTA}${ttl}${db}`);
480
+ const items = answers.map((a) => `- name ${MAGENTA}${a.name}${db} type ${MAGENTA}${this.dnsTypeToString(a.rtype)}${db} class ${MAGENTA}${this.dnsResponseClassToString(a.rclass)}${db} ttl ${MAGENTA}${a.ttl}${db}`).join('\n');
481
+ this.log.debug(`Dgram mDNS server sent response message:\n${items}`);
425
482
  this.emit('sent', response, this.multicastAddress, this.multicastPort);
426
483
  }
427
484
  });
@@ -41,13 +41,24 @@ export function getIpv4InterfaceAddress() {
41
41
  }
42
42
  }
43
43
  }
44
- export function getIpv6InterfaceAddress() {
44
+ export function getIpv6InterfaceAddress(scope = false) {
45
45
  for (const [interfaceName, interfaceDetails] of Object.entries(os.networkInterfaces())) {
46
46
  if (!interfaceName || !interfaceDetails || interfaceDetails.length === 0)
47
47
  continue;
48
48
  for (const detail of interfaceDetails) {
49
- if (detail.family === 'IPv6' && !detail.internal)
50
- return detail.address;
49
+ if (detail.family === 'IPv6' && !detail.internal) {
50
+ const address = detail.address;
51
+ if (!scope)
52
+ return address;
53
+ if (address.includes('%'))
54
+ return address;
55
+ const isWindows = os.platform() === 'win32';
56
+ const zoneId = isWindows ? detail.scopeid : interfaceName;
57
+ if (zoneId !== undefined && zoneId !== null && `${zoneId}`.length > 0) {
58
+ return `${address}%${zoneId}`;
59
+ }
60
+ return address;
61
+ }
51
62
  }
52
63
  }
53
64
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.4.5-dev-20251224-e00e572",
3
+ "version": "3.4.5-dev-20251224-1bb63cb",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.4.5-dev-20251224-e00e572",
9
+ "version": "3.4.5-dev-20251224-1bb63cb",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.15.6",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.4.5-dev-20251224-e00e572",
3
+ "version": "3.4.5-dev-20251224-1bb63cb",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",