matterbridge 3.4.5-dev-20251224-e00e572 → 3.4.5-dev-20251224-457def5

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,15 @@ 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]: Added 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.
40
+ - [mb_mdns]: Added broadcast parameter to allow advertising when multicast is not available.
37
41
 
38
42
  ### Changed
39
43
 
40
44
  - [package]: Updated dependencies.
45
+ - [mdns]: Mdns.sendResponse now accepts an array and sends multi-answer responses.
41
46
 
42
47
  ### Fixed
43
48
 
@@ -1,37 +1,48 @@
1
- import { getParameter, getStringArrayParameter, hasParameter } from '../utils/commandLine.js';
1
+ import { getIntParameter, getParameter, getStringArrayParameter, hasParameter } from '../utils/commandLine.js';
2
2
  import { MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT } from './multicast.js';
3
3
  import { Mdns } from './mdns.js';
4
4
  {
5
5
  if (hasParameter('h') || hasParameter('help')) {
6
- console.log(`Usage: mb_mdns [options]
6
+ console.log(`Copyright (c) Matterbridge. All rights reserved.\n`);
7
+ console.log(`Usage: mb_mdns [options...]
8
+
9
+ If no command line is provided, mb_mdns shows all incoming mDNS records.
7
10
 
8
11
  Options:
9
12
  -h, --help Show this help message and exit.
10
13
  --interfaceName <name> Network interface name to bind to (default all interfaces).
11
14
  --ipv4InterfaceAddress <address> IPv4 address of the network interface to bind to (default: 0.0.0.0).
12
15
  --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).
16
+ --outgoingIpv4InterfaceAddress <address> Outgoing IPv4 address of the network interface (default first external address).
17
+ --outgoingIpv6InterfaceAddress <address> Outgoing IPv6 address of the network interface (default first external address).
18
+ --advertise <interval> Enable matterbridge mDNS advertisement each ms (default interval: 10000ms).
19
+ --query <interval> Enable common mDNS services query each ms (default interval: 10000ms).
20
+ --filter <string...> Filter strings to match in the mDNS record name (default: no filter).
18
21
  -v, --verbose Enable verbose logging (default: disabled).
19
22
 
20
23
  Examples:
21
- # List Matter device commissioner service records only on eth0 interface
24
+ # Listen for Matter device commissioner service records only on eth0 interface
22
25
  mb_mdns --interfaceName eth0 --filter _matterc._udp
23
26
 
24
- # List Matter device service records only on eth0 interface
27
+ # Listen for Matter device discovery service records only on eth0 interface
25
28
  mb_mdns --interfaceName eth0 --filter _matter._tcp
26
29
 
27
- # List both Matter commissioner and device service records only on eth0 interface
28
- mb_mdns --interfaceName eth0 --filter _matterc._udp _matter._tcp
30
+ # Listen for Matter commissioner and discovery service records on all interfaces
31
+ mb_mdns --filter _matterc._udp _matter._tcp
29
32
 
30
33
  # Query for mDNS devices every 10s on a specific interface
31
34
  mb_mdns --interfaceName eth0 --query
35
+
36
+ # Advertise matterbridge._http._tcp.local every 5s with filter
37
+ mb_mdns --advertise 5000 --filter matterbridge._http._tcp.local
38
+
39
+ # Query each 5s and listen for matterbridge._http._tcp.local service records
40
+ mb_mdns --query 5000 --filter matterbridge._http._tcp.local
41
+
32
42
  `);
33
43
  process.exit(0);
34
44
  }
45
+ const { default: pkg } = await import('../../package.json', { with: { type: 'json' } });
35
46
  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
47
  const mdnsIpv6 = new Mdns('mDNS Server udp6', MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT, 'udp6', true, getParameter('interfaceName'), getParameter('ipv6InterfaceAddress') || '::', getParameter('outgoingIpv6InterfaceAddress'));
37
48
  if (hasParameter('v') || hasParameter('verbose')) {
@@ -55,29 +66,71 @@ Examples:
55
66
  mdnsIpv6.logDevices();
56
67
  process.exit(0);
57
68
  }
58
- const queryUdp4 = () => {
59
- mdnsIpv4.sendQuery([
60
- { name: '_matter._tcp.local', type: 12, class: 1, unicastResponse: false },
61
- { name: '_shelly._tcp.local', type: 12, class: 1, unicastResponse: false },
62
- { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: false },
63
- { name: '_services._dns-sd._udp.local', type: 12, class: 1, unicastResponse: false },
64
- ]);
65
- };
66
- const advertiseUdp4 = () => {
67
- const ptrRdata = mdnsIpv4.encodeDnsName('matterbridge._http._tcp.local');
68
- mdnsIpv4.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
69
- };
70
- const queryUdp6 = () => {
71
- mdnsIpv6.sendQuery([
69
+ const query = (mdns) => {
70
+ mdns.log.info('Sending mDNS query for services...');
71
+ mdns.sendQuery([
72
+ { name: '_matterc._udp.local', type: 12, class: 1, unicastResponse: true },
72
73
  { name: '_matter._tcp.local', type: 12, class: 1, unicastResponse: true },
73
74
  { name: '_shelly._tcp.local', type: 12, class: 1, unicastResponse: true },
74
75
  { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: true },
75
76
  { name: '_services._dns-sd._udp.local', type: 12, class: 1, unicastResponse: true },
76
77
  ]);
77
78
  };
78
- const advertiseUdp6 = () => {
79
- const ptrRdata = mdnsIpv6.encodeDnsName('matterbridge._http._tcp.local');
80
- mdnsIpv6.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
79
+ const advertise = (mdns) => {
80
+ mdns.log.info('Sending mDNS advertisement for matterbridge service...');
81
+ const serviceType = '_http._tcp.local';
82
+ const instanceName = 'matterbridge._http._tcp.local';
83
+ const hostName = 'matterbridge.local';
84
+ const port = 8283;
85
+ const ttl = 120;
86
+ const ptrInstanceRdata = mdns.encodeDnsName(instanceName);
87
+ const ptrServiceTypeRdata = mdns.encodeDnsName(serviceType);
88
+ const srvRdata = mdns.encodeSrvRdata(0, 0, port, hostName);
89
+ const txtRdata = mdns.encodeTxtRdata([`version=${pkg.version}`, 'path=/']);
90
+ const answers = [
91
+ { name: '_services._dns-sd._udp.local', rtype: 12, rclass: 1, ttl, rdata: ptrServiceTypeRdata },
92
+ { name: serviceType, rtype: 12, rclass: 1, ttl, rdata: ptrInstanceRdata },
93
+ { name: instanceName, rtype: 33, rclass: 1 | 32768, ttl, rdata: srvRdata },
94
+ { name: instanceName, rtype: 16, rclass: 1 | 32768, ttl, rdata: txtRdata },
95
+ ];
96
+ try {
97
+ const ipv4 = mdns.getIpv4InterfaceAddress(mdns.interfaceName);
98
+ if (ipv4) {
99
+ answers.push({ name: hostName, rtype: 1, rclass: 1 | 32768, ttl, rdata: mdns.encodeA(ipv4) });
100
+ }
101
+ }
102
+ catch (error) {
103
+ mdns.log.warn(`Error sending mDNS advertisement for matterbridge service A record: ${error.message}`);
104
+ }
105
+ try {
106
+ const ipv6 = mdns.getIpv6InterfaceAddress(mdns.interfaceName);
107
+ if (ipv6) {
108
+ answers.push({ name: hostName, rtype: 28, rclass: 1 | 32768, ttl, rdata: mdns.encodeAAAA(ipv6) });
109
+ }
110
+ }
111
+ catch (error) {
112
+ mdns.log.warn(`Error sending mDNS advertisement for matterbridge service AAAA record: ${error.message}`);
113
+ }
114
+ const response = mdns.sendResponse(answers);
115
+ if (hasParameter('broadcast')) {
116
+ try {
117
+ const address = mdns.socketType === 'udp4' ? mdns.getIpv4InterfaceAddress(mdns.interfaceName) : mdns.getIpv6InterfaceAddress(mdns.interfaceName);
118
+ const mask = mdns.socketType === 'udp4' ? mdns.getNetmask(address) : undefined;
119
+ const broadcastAddress = mdns.socketType === 'udp4' ? mdns.getIpv4BroadcastAddress(address, mask) : mdns.getIpv6BroadcastAddress();
120
+ mdns.log.info(`Broadcasting mDNS advertisement for matterbridge service to ${broadcastAddress}...`);
121
+ mdns.socket.send(response, 0, response.length, mdns.multicastPort, broadcastAddress, (error) => {
122
+ if (error) {
123
+ mdns.log.error(`Error broadcasting mDNS advertisement: ${error.message}`);
124
+ }
125
+ else {
126
+ mdns.log.info(`mDNS advertisement broadcasted successfully to ${broadcastAddress}`);
127
+ }
128
+ });
129
+ }
130
+ catch (error) {
131
+ mdns.log.error(`Error broadcasting mDNS advertisement: ${error.message}`);
132
+ }
133
+ }
81
134
  };
82
135
  process.on('SIGINT', () => {
83
136
  cleanupAndLogAndExit();
@@ -85,33 +138,33 @@ Examples:
85
138
  mdnsIpv4.start();
86
139
  mdnsIpv4.on('ready', (address) => {
87
140
  mdnsIpv4.log.info(`mdnsIpv4 server ready on ${address.family} ${address.address}:${address.port}`);
88
- if (getParameter('advertise')) {
89
- advertiseUdp4();
141
+ if (hasParameter('advertise')) {
142
+ advertise(mdnsIpv4);
90
143
  setInterval(() => {
91
- advertiseUdp4();
92
- }, 10000).unref();
144
+ advertise(mdnsIpv4);
145
+ }, getIntParameter('advertise') || 10000).unref();
93
146
  }
94
- if (getParameter('query')) {
95
- queryUdp4();
147
+ if (hasParameter('query')) {
148
+ query(mdnsIpv4);
96
149
  setInterval(() => {
97
- queryUdp4();
98
- }, 10000).unref();
150
+ query(mdnsIpv4);
151
+ }, getIntParameter('query') || 10000).unref();
99
152
  }
100
153
  });
101
154
  mdnsIpv6.start();
102
155
  mdnsIpv6.on('ready', (address) => {
103
156
  mdnsIpv6.log.info(`mdnsIpv6 server ready on ${address.family} ${address.address}:${address.port}`);
104
- if (getParameter('advertise')) {
105
- advertiseUdp6();
157
+ if (hasParameter('advertise')) {
158
+ advertise(mdnsIpv6);
106
159
  setInterval(() => {
107
- advertiseUdp6();
108
- }, 10000).unref();
160
+ advertise(mdnsIpv6);
161
+ }, getIntParameter('advertise') || 10000).unref();
109
162
  }
110
- if (getParameter('query')) {
111
- queryUdp6();
163
+ if (hasParameter('query')) {
164
+ query(mdnsIpv6);
112
165
  setInterval(() => {
113
- queryUdp6();
114
- }, 10000).unref();
166
+ query(mdnsIpv6);
167
+ }, getIntParameter('query') || 10000).unref();
115
168
  }
116
169
  });
117
170
  setTimeout(() => {
@@ -1,4 +1,5 @@
1
1
  import { BLUE, CYAN, db, er, GREEN, idn, MAGENTA, nf, rs } from 'node-ansi-logger';
2
+ import { hasParameter } from '../utils/commandLine.js';
2
3
  import { Multicast } from './multicast.js';
3
4
  export var DnsRecordType;
4
5
  (function (DnsRecordType) {
@@ -271,6 +272,56 @@ export class Mdns extends Multicast {
271
272
  });
272
273
  return Buffer.concat([...buffers, Buffer.from([0])]);
273
274
  }
275
+ encodeTxtRdata(txt) {
276
+ const parts = txt.map((entry) => {
277
+ const value = Buffer.from(entry, 'utf8');
278
+ if (value.length > 255)
279
+ throw new Error(`TXT entry too long: ${entry}`);
280
+ return Buffer.concat([Buffer.from([value.length]), value]);
281
+ });
282
+ return Buffer.concat(parts);
283
+ }
284
+ encodeSrvRdata(priority, weight, port, target) {
285
+ const fixed = Buffer.alloc(6);
286
+ fixed.writeUInt16BE(priority, 0);
287
+ fixed.writeUInt16BE(weight, 2);
288
+ fixed.writeUInt16BE(port, 4);
289
+ return Buffer.concat([fixed, this.encodeDnsName(target)]);
290
+ }
291
+ encodeA(ipv4) {
292
+ const parts = ipv4.split('.').map((p) => Number(p));
293
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
294
+ throw new Error(`Invalid IPv4 address: ${ipv4}`);
295
+ }
296
+ return Buffer.from(parts);
297
+ }
298
+ encodeAAAA(ipv6WithOptionalScope) {
299
+ const ipv6 = ipv6WithOptionalScope.split('%')[0];
300
+ const [left, right] = ipv6.split('::');
301
+ const leftParts = left ? left.split(':').filter(Boolean) : [];
302
+ const rightParts = right ? right.split(':').filter(Boolean) : [];
303
+ if (ipv6.includes('::')) {
304
+ const missing = 8 - (leftParts.length + rightParts.length);
305
+ if (missing < 0)
306
+ throw new Error(`Invalid IPv6 address: ${ipv6WithOptionalScope}`);
307
+ const groups = [...leftParts, ...Array(missing).fill('0'), ...rightParts];
308
+ return Buffer.from(groups.flatMap((g) => {
309
+ const word = parseInt(g || '0', 16);
310
+ if (!Number.isFinite(word) || word < 0 || word > 0xffff)
311
+ throw new Error(`Invalid IPv6 group: ${g}`);
312
+ return [(word >> 8) & 0xff, word & 0xff];
313
+ }));
314
+ }
315
+ const groups = ipv6.split(':');
316
+ if (groups.length !== 8)
317
+ throw new Error(`Invalid IPv6 address: ${ipv6WithOptionalScope}`);
318
+ return Buffer.from(groups.flatMap((g) => {
319
+ const word = parseInt(g || '0', 16);
320
+ if (!Number.isFinite(word) || word < 0 || word > 0xffff)
321
+ throw new Error(`Invalid IPv6 group: ${g}`);
322
+ return [(word >> 8) & 0xff, word & 0xff];
323
+ }));
324
+ }
274
325
  decodeResourceRecord(msg, offset) {
275
326
  const nameResult = this.decodeDnsName(msg, offset);
276
327
  const name = nameResult.name;
@@ -383,8 +434,10 @@ export class Mdns extends Multicast {
383
434
  return Buffer.concat([qname, qfields]);
384
435
  });
385
436
  const query = Buffer.concat([header, ...questionBuffers]);
386
- const decoded = this.decodeMdnsMessage(query);
387
- this.logMdnsMessage(decoded);
437
+ if (hasParameter('v') || hasParameter('verbose')) {
438
+ const decoded = this.decodeMdnsMessage(query);
439
+ this.logMdnsMessage(decoded);
440
+ }
388
441
  this.socket.send(query, 0, query.length, this.multicastPort, this.multicastAddress, (error) => {
389
442
  if (error) {
390
443
  this.log.error(`Dgram mDNS server failed to send query message: ${error.message}`);
@@ -398,33 +451,46 @@ export class Mdns extends Multicast {
398
451
  this.emit('sent', query, this.multicastAddress, this.multicastPort);
399
452
  }
400
453
  });
454
+ return query;
401
455
  }
402
- sendResponse(name, rtype, rclass, ttl, rdata) {
456
+ sendResponse(answers) {
457
+ if (!Array.isArray(answers) || answers.length === 0) {
458
+ throw new Error('sendResponse requires a non-empty answers array');
459
+ }
403
460
  const header = Buffer.alloc(12);
404
461
  header.writeUInt16BE(0, 0);
405
462
  header.writeUInt16BE(0x8400, 2);
406
463
  header.writeUInt16BE(0, 4);
407
- header.writeUInt16BE(1, 6);
464
+ header.writeUInt16BE(answers.length, 6);
408
465
  header.writeUInt16BE(0, 8);
409
466
  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]);
467
+ const answerBuffers = answers.map(({ name, rtype, rclass, ttl, rdata }) => {
468
+ const aname = this.encodeDnsName(name);
469
+ const answerFixed = Buffer.alloc(10);
470
+ answerFixed.writeUInt16BE(rtype, 0);
471
+ answerFixed.writeUInt16BE(rclass, 2);
472
+ answerFixed.writeUInt32BE(ttl, 4);
473
+ answerFixed.writeUInt16BE(rdata.length, 8);
474
+ return Buffer.concat([aname, answerFixed, rdata]);
475
+ });
476
+ const response = Buffer.concat([header, ...answerBuffers]);
477
+ if (hasParameter('v') || hasParameter('verbose')) {
478
+ const decoded = this.decodeMdnsMessage(response);
479
+ this.logMdnsMessage(decoded);
480
+ }
418
481
  this.socket.send(response, 0, response.length, this.multicastPort, this.multicastAddress, (error) => {
419
482
  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}`);
483
+ 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');
484
+ 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
485
  this.emit('error', error);
422
486
  }
423
487
  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}`);
488
+ 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');
489
+ this.log.debug(`Dgram mDNS server sent response message:\n${items}`);
425
490
  this.emit('sent', response, this.multicastAddress, this.multicastPort);
426
491
  }
427
492
  });
493
+ return response;
428
494
  }
429
495
  dnsTypeToString(type) {
430
496
  const typeMap = {
@@ -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-457def5",
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-457def5",
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-457def5",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",