matterbridge
Version:
Matterbridge plugin manager for Matter
760 lines • 37 kB
JavaScript
/**
* @description This file contains the class Mdns.
* @file mdns.ts
* @author Luca Liguori
* @created 2025-03-22
* @version 1.0.0
* @license Apache-2.0
*
* Copyright 2025, 2026, 2027 Luca Liguori.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// AnsiLogger imports
import { BLUE, CYAN, db, er, GREEN, idn, MAGENTA, nf, rs } from 'node-ansi-logger';
// Net imports
import { Multicast } from './multicast.js';
export var DnsRecordType;
(function (DnsRecordType) {
DnsRecordType[DnsRecordType["A"] = 1] = "A";
DnsRecordType[DnsRecordType["NS"] = 2] = "NS";
DnsRecordType[DnsRecordType["MD"] = 3] = "MD";
DnsRecordType[DnsRecordType["MF"] = 4] = "MF";
DnsRecordType[DnsRecordType["CNAME"] = 5] = "CNAME";
DnsRecordType[DnsRecordType["SOA"] = 6] = "SOA";
DnsRecordType[DnsRecordType["MB"] = 7] = "MB";
DnsRecordType[DnsRecordType["MG"] = 8] = "MG";
DnsRecordType[DnsRecordType["MR"] = 9] = "MR";
DnsRecordType[DnsRecordType["NULL"] = 10] = "NULL";
DnsRecordType[DnsRecordType["WKS"] = 11] = "WKS";
DnsRecordType[DnsRecordType["PTR"] = 12] = "PTR";
DnsRecordType[DnsRecordType["HINFO"] = 13] = "HINFO";
DnsRecordType[DnsRecordType["MINFO"] = 14] = "MINFO";
DnsRecordType[DnsRecordType["MX"] = 15] = "MX";
DnsRecordType[DnsRecordType["TXT"] = 16] = "TXT";
DnsRecordType[DnsRecordType["RP"] = 17] = "RP";
DnsRecordType[DnsRecordType["AFSDB"] = 18] = "AFSDB";
DnsRecordType[DnsRecordType["X25"] = 19] = "X25";
DnsRecordType[DnsRecordType["ISDN"] = 20] = "ISDN";
DnsRecordType[DnsRecordType["RT"] = 21] = "RT";
DnsRecordType[DnsRecordType["NSAP"] = 22] = "NSAP";
DnsRecordType[DnsRecordType["NSAP_PTR"] = 23] = "NSAP_PTR";
DnsRecordType[DnsRecordType["SIG"] = 24] = "SIG";
DnsRecordType[DnsRecordType["KEY"] = 25] = "KEY";
DnsRecordType[DnsRecordType["PX"] = 26] = "PX";
DnsRecordType[DnsRecordType["GPOS"] = 27] = "GPOS";
DnsRecordType[DnsRecordType["AAAA"] = 28] = "AAAA";
DnsRecordType[DnsRecordType["LOC"] = 29] = "LOC";
DnsRecordType[DnsRecordType["NXT"] = 30] = "NXT";
DnsRecordType[DnsRecordType["EID"] = 31] = "EID";
DnsRecordType[DnsRecordType["NIMLOC"] = 32] = "NIMLOC";
DnsRecordType[DnsRecordType["SRV"] = 33] = "SRV";
DnsRecordType[DnsRecordType["ATMA"] = 34] = "ATMA";
DnsRecordType[DnsRecordType["NAPTR"] = 35] = "NAPTR";
DnsRecordType[DnsRecordType["KX"] = 36] = "KX";
DnsRecordType[DnsRecordType["CERT"] = 37] = "CERT";
DnsRecordType[DnsRecordType["A6"] = 38] = "A6";
DnsRecordType[DnsRecordType["DNAME"] = 39] = "DNAME";
DnsRecordType[DnsRecordType["SINK"] = 40] = "SINK";
DnsRecordType[DnsRecordType["OPT"] = 41] = "OPT";
DnsRecordType[DnsRecordType["APL"] = 42] = "APL";
DnsRecordType[DnsRecordType["DS"] = 43] = "DS";
DnsRecordType[DnsRecordType["SSHFP"] = 44] = "SSHFP";
DnsRecordType[DnsRecordType["IPSECKEY"] = 45] = "IPSECKEY";
DnsRecordType[DnsRecordType["RRSIG"] = 46] = "RRSIG";
DnsRecordType[DnsRecordType["NSEC"] = 47] = "NSEC";
DnsRecordType[DnsRecordType["DNSKEY"] = 48] = "DNSKEY";
DnsRecordType[DnsRecordType["DHCID"] = 49] = "DHCID";
DnsRecordType[DnsRecordType["NSEC3"] = 50] = "NSEC3";
DnsRecordType[DnsRecordType["NSEC3PARAM"] = 51] = "NSEC3PARAM";
DnsRecordType[DnsRecordType["TLSA"] = 52] = "TLSA";
DnsRecordType[DnsRecordType["SMIMEA"] = 53] = "SMIMEA";
DnsRecordType[DnsRecordType["HIP"] = 55] = "HIP";
DnsRecordType[DnsRecordType["NINFO"] = 56] = "NINFO";
DnsRecordType[DnsRecordType["RKEY"] = 57] = "RKEY";
DnsRecordType[DnsRecordType["TALINK"] = 58] = "TALINK";
DnsRecordType[DnsRecordType["CDS"] = 59] = "CDS";
DnsRecordType[DnsRecordType["CDNSKEY"] = 60] = "CDNSKEY";
DnsRecordType[DnsRecordType["OPENPGPKEY"] = 61] = "OPENPGPKEY";
DnsRecordType[DnsRecordType["CSYNC"] = 62] = "CSYNC";
DnsRecordType[DnsRecordType["ZONEMD"] = 63] = "ZONEMD";
DnsRecordType[DnsRecordType["SVCB"] = 64] = "SVCB";
DnsRecordType[DnsRecordType["HTTPS"] = 65] = "HTTPS";
DnsRecordType[DnsRecordType["SPF"] = 99] = "SPF";
DnsRecordType[DnsRecordType["UINFO"] = 100] = "UINFO";
DnsRecordType[DnsRecordType["UID"] = 101] = "UID";
DnsRecordType[DnsRecordType["GID"] = 102] = "GID";
DnsRecordType[DnsRecordType["UNSPEC"] = 103] = "UNSPEC";
DnsRecordType[DnsRecordType["NID"] = 104] = "NID";
DnsRecordType[DnsRecordType["L32"] = 105] = "L32";
DnsRecordType[DnsRecordType["L64"] = 106] = "L64";
DnsRecordType[DnsRecordType["LP"] = 107] = "LP";
DnsRecordType[DnsRecordType["EUI48"] = 108] = "EUI48";
DnsRecordType[DnsRecordType["EUI64"] = 109] = "EUI64";
DnsRecordType[DnsRecordType["TKEY"] = 249] = "TKEY";
DnsRecordType[DnsRecordType["TSIG"] = 250] = "TSIG";
DnsRecordType[DnsRecordType["IXFR"] = 251] = "IXFR";
DnsRecordType[DnsRecordType["AXFR"] = 252] = "AXFR";
DnsRecordType[DnsRecordType["MAILB"] = 253] = "MAILB";
DnsRecordType[DnsRecordType["MAILA"] = 254] = "MAILA";
DnsRecordType[DnsRecordType["ANY"] = 255] = "ANY";
DnsRecordType[DnsRecordType["URI"] = 256] = "URI";
DnsRecordType[DnsRecordType["CAA"] = 257] = "CAA";
DnsRecordType[DnsRecordType["AVC"] = 258] = "AVC";
DnsRecordType[DnsRecordType["DOA"] = 259] = "DOA";
DnsRecordType[DnsRecordType["AMTRELAY"] = 260] = "AMTRELAY";
DnsRecordType[DnsRecordType["ZONEVERSION"] = 261] = "ZONEVERSION";
// 262-32767 are unassigned/reserved
DnsRecordType[DnsRecordType["TA"] = 32768] = "TA";
DnsRecordType[DnsRecordType["DLV"] = 32769] = "DLV";
})(DnsRecordType || (DnsRecordType = {}));
export var DnsClass;
(function (DnsClass) {
DnsClass[DnsClass["IN"] = 1] = "IN";
DnsClass[DnsClass["CH"] = 3] = "CH";
DnsClass[DnsClass["HS"] = 4] = "HS";
DnsClass[DnsClass["ANY"] = 255] = "ANY";
})(DnsClass || (DnsClass = {}));
export var DnsClassFlag;
(function (DnsClassFlag) {
DnsClassFlag[DnsClassFlag["FLUSH"] = 32768] = "FLUSH";
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
DnsClassFlag[DnsClassFlag["QU"] = 32768] = "QU";
})(DnsClassFlag || (DnsClassFlag = {}));
export class Mdns extends Multicast {
deviceQueries = new Map();
deviceResponses = new Map();
/**
* Creates an instance of the Mdns class.
*
* @param {string} name - The internal name of the mDNS server for the logs.
* @param {string} multicastAddress - The multicast address for mDNS (i.e. 224.0.0.251 for udp4 or ff02::fb for udp6).
* @param {number} multicastPort - The port for mDNS (i.e. 5353).
* @param {('udp4' | 'udp6')} socketType - The type of socket to create (either 'udp4' or 'udp6').
* @param {boolean} [reuseAddr] - Whether to reuse the address. Defaults to true.
* @param {string} [interfaceName] - The optional name of the network interface to use.
* @param {string} [interfaceAddress] - The optional IP address of the network interface to use.
*/
constructor(name, multicastAddress, multicastPort, socketType, reuseAddr = true, interfaceName, interfaceAddress) {
super(name, multicastAddress, multicastPort, socketType, reuseAddr, interfaceName, interfaceAddress);
}
onQuery(rinfo, _query) {
this.log.debug(`mDNS query received from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
}
onResponse(rinfo, _response) {
this.log.debug(`mDNS response received from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
}
onMessage(msg, rinfo) {
this.log.info(`Dgram mDNS server received a mDNS message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
try {
const result = this.decodeMdnsMessage(msg);
if (result.qr === 0) {
this.deviceQueries.set(rinfo.address, { rinfo, query: result });
this.onQuery(rinfo, result);
}
else {
const ptr = result.answers?.find((record) => record.name === '_shelly._tcp.local' && record.type === 12 /* DnsRecordType.PTR */) ||
result.answers?.find((record) => record.name === '_http._tcp.local' && record.type === 12 /* DnsRecordType.PTR */) ||
result.answers?.find((record) => record.type === 12 /* DnsRecordType.PTR */) ||
result.answers?.find((record) => record.type === 16 /* DnsRecordType.TXT */) ||
result.answers
? result.answers[0]
: undefined; // Fallback to the first answer if no PTR or TXT found
this.deviceResponses.set(rinfo.address, { rinfo, response: result, dataPTR: ptr?.type === 12 /* DnsRecordType.PTR */ ? ptr?.data : ptr?.name });
this.onResponse(rinfo, result);
}
this.logMdnsMessage(result);
}
catch (error) {
this.log.error(`Error decoding mDNS message: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Decodes an mDNS message, including the header, question section, answer section,
* authority section, and additional section.
*
* @param {Buffer} msg - The raw mDNS message buffer.
* @returns {MdnsMessage} An object representing the decoded mDNS message.
* @throws Error if the message is too short.
*/
decodeMdnsMessage(msg) {
if (msg.length < 12) {
throw new Error('mDNS message too short');
}
const id = msg.readUInt16BE(0);
const flags = msg.readUInt16BE(2);
const qr = (flags & 0x8000) >> 15; // Bit 15: 0=query, 1=response.
const opcode = (flags & 0x7800) >> 11; // Bits 14-11.
const aa = Boolean(flags & 0x0400); // Bit 10.
const tc = Boolean(flags & 0x0200); // Bit 9.
const rd = Boolean(flags & 0x0100); // Bit 8.
const ra = Boolean(flags & 0x0080); // Bit 7.
const z = (flags & 0x0070) >> 4; // Bits 6-4.
const rcode = flags & 0x000f; // Bits 3-0.
const qdCount = msg.readUInt16BE(4);
const anCount = msg.readUInt16BE(6);
const nsCount = msg.readUInt16BE(8);
const arCount = msg.readUInt16BE(10);
const mdnsMessage = {
id,
qr,
opcode,
aa,
tc,
rd,
ra,
z,
rcode,
qdCount,
anCount,
nsCount,
arCount,
questions: [],
answers: [],
authorities: [],
additionals: [],
};
let offset = 12;
// Decode the question section.
for (let i = 0; i < qdCount; i++) {
const qnameResult = this.decodeDnsName(msg, offset);
const qname = qnameResult.name;
offset = qnameResult.newOffset;
const qtype = msg.readUInt16BE(offset);
offset += 2;
const qclass = msg.readUInt16BE(offset);
offset += 2;
mdnsMessage.questions?.push({ name: qname, type: qtype, class: qclass });
}
// Decode the answer section.
for (let i = 0; i < anCount; i++) {
const rrResult = this.decodeResourceRecord(msg, offset);
mdnsMessage.answers?.push(rrResult.record);
offset = rrResult.newOffset;
}
// Decode the authority (NS) section.
for (let i = 0; i < nsCount; i++) {
const rrResult = this.decodeResourceRecord(msg, offset);
mdnsMessage.authorities?.push(rrResult.record);
offset = rrResult.newOffset;
}
// Decode the additional records section.
for (let i = 0; i < arCount; i++) {
const rrResult = this.decodeResourceRecord(msg, offset);
mdnsMessage.additionals?.push(rrResult.record);
offset = rrResult.newOffset;
}
return mdnsMessage;
}
/**
* Decodes a DNS name from a buffer, handling compression.
*
* @param {Buffer} msg - The full mDNS message buffer.
* @param {number} offset - The offset at which the DNS name starts.
* @returns {{ name: string; newOffset: number }} An object with the decoded name and the new offset.
* @throws Error if the offset exceeds the buffer length or too many iterations are performed.
*/
decodeDnsName(msg, offset) {
const labels = [];
let jumped = false;
let originalOffset = offset;
let iterations = 0; // Prevent infinite loops
while (true) {
// Safety guard: prevent infinite loops in malformed messages.
if (iterations++ > 1000) {
throw new Error('Too many iterations while decoding DNS name. Possible malformed message.');
}
// Check that offset is within buffer bounds.
if (offset >= msg.length) {
throw new Error('Offset exceeds buffer length while decoding DNS name.');
}
const len = msg.readUInt8(offset);
if (len === 0) {
offset++;
break;
}
// Check for pointer (first two bits are 11)
if ((len & 0xc0) === 0xc0) {
// Ensure the pointer has two bytes available.
if (offset + 1 >= msg.length) {
throw new Error('Incomplete pointer encountered while decoding DNS name.');
}
const pointer = ((len & 0x3f) << 8) | msg.readUInt8(offset + 1);
if (!jumped) {
originalOffset = offset + 2;
}
offset = pointer;
jumped = true;
continue;
}
offset++;
// Check that the label length doesn't go beyond the buffer.
if (offset + len > msg.length) {
throw new Error('Label length exceeds buffer bounds while decoding DNS name.');
}
labels.push(msg.toString('utf8', offset, offset + len));
offset += len;
}
return { name: labels.join('.'), newOffset: jumped ? originalOffset : offset };
}
/**
* Encodes a domain name into the DNS label format.
*
* For example, "example.local" becomes:
* [7] "example" [5] "local" [0]
*
* @param {string} name - The domain name to encode.
* @returns {Buffer} The encoded domain name as a Buffer.
*/
encodeDnsName(name) {
const labels = name.split('.');
const buffers = labels.map((label) => {
const lenBuf = Buffer.alloc(1);
lenBuf.writeUInt8(label.length, 0);
return Buffer.concat([lenBuf, Buffer.from(label)]);
});
// Append the null byte to terminate the name.
return Buffer.concat([...buffers, Buffer.from([0])]);
}
/**
* Decodes a DNS resource record.
*
* @param {Buffer} msg - The full mDNS message buffer.
* @param {number} offset - The offset at which the resource record starts.
* @returns {{ record: MdnsRecord; newOffset: number }} An object containing the decoded record and the new offset.
*/
decodeResourceRecord(msg, offset) {
// Decode the NAME field (which may be compressed)
const nameResult = this.decodeDnsName(msg, offset);
const name = nameResult.name;
offset = nameResult.newOffset;
// Read TYPE (16 bits), CLASS (16 bits), TTL (32 bits), and RDLENGTH (16 bits)
const type = msg.readUInt16BE(offset);
offset += 2;
const rrclass = msg.readUInt16BE(offset);
offset += 2;
const ttl = msg.readUInt32BE(offset);
offset += 4;
const rdlength = msg.readUInt16BE(offset);
offset += 2;
let data = '';
if (type === 12 /* DnsRecordType.PTR */) {
// PTR record (type 12): decode its RDATA as a domain name.
const ptrResult = this.decodeDnsName(msg, offset);
data = ptrResult.name;
offset += rdlength;
}
else if (type === 16 /* DnsRecordType.TXT */) {
// TXT record: may consist of one or more length-prefixed strings.
const txtStrings = [];
const end = offset + rdlength;
while (offset < end) {
const txtLen = msg[offset];
offset++;
const txt = msg.slice(offset, offset + txtLen).toString('utf8');
txtStrings.push(txt);
offset += txtLen;
}
data = txtStrings.join(', ');
}
else if (type === 33 /* DnsRecordType.SRV */) {
// SRV record (type === 33): consists of 2 bytes for priority, 2 for weight, 2 for port, followed by the target domain name.
const priority = msg.readUInt16BE(offset);
const weight = msg.readUInt16BE(offset + 2);
const port = msg.readUInt16BE(offset + 4);
offset += 6;
const srvTargetResult = this.decodeDnsName(msg, offset);
data = JSON.stringify({
priority,
weight,
port,
target: srvTargetResult.name,
});
offset = srvTargetResult.newOffset;
}
else if (type === 1 /* DnsRecordType.A */) {
// A record (type 1): an IPv4 address stored in 4 bytes.
const ipBytes = msg.slice(offset, offset + 4);
data = Array.from(ipBytes).join('.');
offset += 4;
}
else if (type === 28 /* DnsRecordType.AAAA */) {
// AAAA record (type 28): IPv6 address stored in 16 bytes.
const ipBytes = msg.slice(offset, offset + 16);
// Convert the 16 bytes into an IPv6 address string (colon-separated)
const ipv6Parts = [];
for (let i = 0; i < 16; i += 2) {
ipv6Parts.push(ipBytes.readUInt16BE(i).toString(16));
}
data = ipv6Parts.join(':');
offset += 16;
}
else if (type === 47 /* DnsRecordType.NSEC */) {
// NSEC record: RDATA consists of:
// - Next Domain Name (in DNS label format)
// - Type Bit Maps (variable length)
const { name: nextDomain, newOffset } = this.decodeDnsName(msg, offset);
const nextDomainLength = newOffset - offset;
offset = newOffset;
// Calculate the remaining length for the type bit maps.
const bitmapLength = rdlength - nextDomainLength;
const bitmapData = msg.slice(offset, offset + bitmapLength);
const types = [];
let bitmapOffset = 0;
while (bitmapOffset < bitmapData.length) {
const windowBlock = bitmapData[bitmapOffset];
const windowLength = bitmapData[bitmapOffset + 1];
bitmapOffset += 2;
for (let i = 0; i < windowLength; i++) {
const octet = bitmapData[bitmapOffset + i];
for (let bit = 0; bit < 8; bit++) {
if (octet & (0x80 >> bit)) {
const typeCode = windowBlock * 256 + i * 8 + bit;
types.push(this.dnsTypeToString(typeCode));
}
}
}
bitmapOffset += windowLength;
}
data = JSON.stringify({
nextDomain,
types,
});
offset += bitmapLength;
}
else {
// Fall back
data = msg.slice(offset, offset + rdlength).toString('hex');
offset += rdlength;
}
return {
record: { name, type, class: rrclass, ttl, data },
newOffset: offset,
};
}
/**
* Sends a DNS query with multiple questions.
*
* @param {Array<{ name: string; type: number; class: number; unicastResponse?: boolean }>} questions - Array of questions
*
* @remarks
* Each question should have a name (e.g., "_http._tcp.local"), type (e.g., DnsRecordType.PTR), class (e.g., DnsClass.IN),
* and an optional unicastResponse flag (this will add the DnsClassFlag.QU flag to the query).
*/
sendQuery(questions) {
const header = Buffer.alloc(12);
header.writeUInt16BE(0, 0); // ID
header.writeUInt16BE(0, 2); // Flags
header.writeUInt16BE(questions.length, 4); // QDCOUNT
header.writeUInt16BE(0, 6); // ANCOUNT
header.writeUInt16BE(0, 8); // NSCOUNT
header.writeUInt16BE(0, 10); // ARCOUNT
const questionBuffers = questions.map(({ name, type: qtype, class: qclass, unicastResponse = false }) => {
const qname = this.encodeDnsName(name);
const qfields = Buffer.alloc(4);
qfields.writeUInt16BE(qtype, 0);
qfields.writeUInt16BE(unicastResponse ? qclass | 32768 /* DnsClassFlag.QU */ : qclass, 2);
return Buffer.concat([qname, qfields]);
});
const query = Buffer.concat([header, ...questionBuffers]);
const decoded = this.decodeMdnsMessage(query);
this.logMdnsMessage(decoded);
this.socket.send(query, 0, query.length, this.multicastPort, this.multicastAddress, (error) => {
if (error) {
this.log.error(`Dgram mDNS server failed to send query message: ${error.message}`);
this.emit('error', error);
}
else {
const names = questions
.map((q) => `- name ${MAGENTA}${q.name}${db} type ${MAGENTA}${this.dnsTypeToString(q.type)}${db} class ${MAGENTA}${this.dnsQuestionClassToString(q.class)}${db} unicastResponse ${MAGENTA}${q.unicastResponse}${db}`)
.join('\n');
this.log.debug(`Dgram mDNS server sent query message:\n${names}`);
this.emit('sent', query, this.multicastAddress, this.multicastPort);
}
});
}
/**
* Constructs an mDNS response packet and sends it to the multicast address and port.
*
* @param {string} name - The domain name being responded to (e.g., "example.local").
* @param {number} rtype - The response type (e.g., 1 for A, 28 for AAAA, etc.).
* @param {number} rclass - The response class (typically 1 for IN).
* @param {number} ttl - The time-to-live for the answer record.
* @param {Buffer} rdata - The resource data for the response (e.g., 4 bytes for an A record IPv4 address).
*
* @example
* const ptrRdata = mdnsIpv4.encodeDnsName('matterbridge._http._tcp.local');
* mdnsIpv4.sendResponse('_http._tcp.local', DnsRecordType.PTR, DnsClass.IN, 120, ptrRdata);
*/
sendResponse(name, rtype, rclass, ttl, rdata) {
// Create a 12-byte DNS header.
const header = Buffer.alloc(12);
header.writeUInt16BE(0, 0); // ID is set to 0 in mDNS.
// Set flags: QR (response) bit and AA (authoritative answer) bit.
header.writeUInt16BE(0x8400, 2);
header.writeUInt16BE(0, 4); // QDCOUNT: 0 questions in response.
header.writeUInt16BE(1, 6); // ANCOUNT: 1 answer record.
header.writeUInt16BE(0, 8); // NSCOUNT: 0 authority records.
header.writeUInt16BE(0, 10); // ARCOUNT: 0 additional records.
// Encode the domain name in DNS label format.
const aname = this.encodeDnsName(name);
// Prepare the fixed part of the answer record:
// - 2 bytes for qtype,
// - 2 bytes for qclass,
// - 4 bytes for TTL,
// - 2 bytes for RDLENGTH (length of the rdata).
const answerFixed = Buffer.alloc(10);
answerFixed.writeUInt16BE(rtype, 0); // Record type.
answerFixed.writeUInt16BE(rclass, 2); // Record class.
answerFixed.writeUInt32BE(ttl, 4); // Time-to-live.
answerFixed.writeUInt16BE(rdata.length, 8); // RDLENGTH.
// Concatenate the answer: encoded name, fixed fields, and resource data.
const answer = Buffer.concat([aname, answerFixed, rdata]);
// Concatenate header and answer to form the complete mDNS response packet.
const response = Buffer.concat([header, answer]);
// Send the response packet via the socket.
this.socket.send(response, 0, response.length, this.multicastPort, this.multicastAddress, (error) => {
if (error) {
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}`);
this.emit('error', error);
}
else {
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}`);
this.emit('sent', response, this.multicastAddress, this.multicastPort);
}
});
}
/**
* Converts a DNS record type numeric value to its string representation.
*
* @param {number} type - The numeric DNS record type.
* @returns {string} The string representation of the record type.
*/
dnsTypeToString(type) {
const typeMap = {
[1 /* DnsRecordType.A */]: 'A',
[2 /* DnsRecordType.NS */]: 'NS',
[3 /* DnsRecordType.MD */]: 'MD',
[4 /* DnsRecordType.MF */]: 'MF',
[5 /* DnsRecordType.CNAME */]: 'CNAME',
[6 /* DnsRecordType.SOA */]: 'SOA',
[7 /* DnsRecordType.MB */]: 'MB',
[8 /* DnsRecordType.MG */]: 'MG',
[9 /* DnsRecordType.MR */]: 'MR',
[10 /* DnsRecordType.NULL */]: 'NULL',
[11 /* DnsRecordType.WKS */]: 'WKS',
[12 /* DnsRecordType.PTR */]: 'PTR',
[13 /* DnsRecordType.HINFO */]: 'HINFO',
[14 /* DnsRecordType.MINFO */]: 'MINFO',
[15 /* DnsRecordType.MX */]: 'MX',
[16 /* DnsRecordType.TXT */]: 'TXT',
[17 /* DnsRecordType.RP */]: 'RP',
[18 /* DnsRecordType.AFSDB */]: 'AFSDB',
[19 /* DnsRecordType.X25 */]: 'X25',
[20 /* DnsRecordType.ISDN */]: 'ISDN',
[21 /* DnsRecordType.RT */]: 'RT',
[22 /* DnsRecordType.NSAP */]: 'NSAP',
[23 /* DnsRecordType.NSAP_PTR */]: 'NSAP_PTR',
[24 /* DnsRecordType.SIG */]: 'SIG',
[25 /* DnsRecordType.KEY */]: 'KEY',
[26 /* DnsRecordType.PX */]: 'PX',
[27 /* DnsRecordType.GPOS */]: 'GPOS',
[28 /* DnsRecordType.AAAA */]: 'AAAA',
[29 /* DnsRecordType.LOC */]: 'LOC',
[30 /* DnsRecordType.NXT */]: 'NXT',
[31 /* DnsRecordType.EID */]: 'EID',
[32 /* DnsRecordType.NIMLOC */]: 'NIMLOC',
[33 /* DnsRecordType.SRV */]: 'SRV',
[34 /* DnsRecordType.ATMA */]: 'ATMA',
[35 /* DnsRecordType.NAPTR */]: 'NAPTR',
[36 /* DnsRecordType.KX */]: 'KX',
[37 /* DnsRecordType.CERT */]: 'CERT',
[38 /* DnsRecordType.A6 */]: 'A6',
[39 /* DnsRecordType.DNAME */]: 'DNAME',
[40 /* DnsRecordType.SINK */]: 'SINK',
[41 /* DnsRecordType.OPT */]: 'OPT',
[42 /* DnsRecordType.APL */]: 'APL',
[43 /* DnsRecordType.DS */]: 'DS',
[44 /* DnsRecordType.SSHFP */]: 'SSHFP',
[45 /* DnsRecordType.IPSECKEY */]: 'IPSECKEY',
[46 /* DnsRecordType.RRSIG */]: 'RRSIG',
[47 /* DnsRecordType.NSEC */]: 'NSEC',
[48 /* DnsRecordType.DNSKEY */]: 'DNSKEY',
[49 /* DnsRecordType.DHCID */]: 'DHCID',
[50 /* DnsRecordType.NSEC3 */]: 'NSEC3',
[51 /* DnsRecordType.NSEC3PARAM */]: 'NSEC3PARAM',
[52 /* DnsRecordType.TLSA */]: 'TLSA',
[53 /* DnsRecordType.SMIMEA */]: 'SMIMEA',
[55 /* DnsRecordType.HIP */]: 'HIP',
[56 /* DnsRecordType.NINFO */]: 'NINFO',
[57 /* DnsRecordType.RKEY */]: 'RKEY',
[58 /* DnsRecordType.TALINK */]: 'TALINK',
[59 /* DnsRecordType.CDS */]: 'CDS',
[60 /* DnsRecordType.CDNSKEY */]: 'CDNSKEY',
[61 /* DnsRecordType.OPENPGPKEY */]: 'OPENPGPKEY',
[62 /* DnsRecordType.CSYNC */]: 'CSYNC',
[63 /* DnsRecordType.ZONEMD */]: 'ZONEMD',
[64 /* DnsRecordType.SVCB */]: 'SVCB',
[65 /* DnsRecordType.HTTPS */]: 'HTTPS',
[99 /* DnsRecordType.SPF */]: 'SPF',
[100 /* DnsRecordType.UINFO */]: 'UINFO',
[101 /* DnsRecordType.UID */]: 'UID',
[102 /* DnsRecordType.GID */]: 'GID',
[103 /* DnsRecordType.UNSPEC */]: 'UNSPEC',
[104 /* DnsRecordType.NID */]: 'NID',
[105 /* DnsRecordType.L32 */]: 'L32',
[106 /* DnsRecordType.L64 */]: 'L64',
[107 /* DnsRecordType.LP */]: 'LP',
[108 /* DnsRecordType.EUI48 */]: 'EUI48',
[109 /* DnsRecordType.EUI64 */]: 'EUI64',
[249 /* DnsRecordType.TKEY */]: 'TKEY',
[250 /* DnsRecordType.TSIG */]: 'TSIG',
[251 /* DnsRecordType.IXFR */]: 'IXFR',
[252 /* DnsRecordType.AXFR */]: 'AXFR',
[253 /* DnsRecordType.MAILB */]: 'MAILB',
[254 /* DnsRecordType.MAILA */]: 'MAILA',
[255 /* DnsRecordType.ANY */]: 'ANY',
[256 /* DnsRecordType.URI */]: 'URI',
[257 /* DnsRecordType.CAA */]: 'CAA',
[258 /* DnsRecordType.AVC */]: 'AVC',
[259 /* DnsRecordType.DOA */]: 'DOA',
[260 /* DnsRecordType.AMTRELAY */]: 'AMTRELAY',
[261 /* DnsRecordType.ZONEVERSION */]: 'ZONEVERSION',
[32768 /* DnsRecordType.TA */]: 'TA',
[32769 /* DnsRecordType.DLV */]: 'DLV',
};
return typeMap[type] ?? `TYPE${type}`;
}
/**
* Converts a DNS response class numeric value to its string representation.
*
* @param {number} cls - The numeric DNS class.
* @returns {string} The string representation of the DNS class.
*/
dnsResponseClassToString(cls) {
const isFlush = !!(cls & 32768 /* DnsClassFlag.FLUSH */);
const baseClass = cls & 0x7fff;
let classStr;
switch (baseClass) {
case 1 /* DnsClass.IN */:
classStr = 'IN';
break;
case 3 /* DnsClass.CH */:
classStr = 'CH';
break;
case 4 /* DnsClass.HS */:
classStr = 'HS';
break;
case 255 /* DnsClass.ANY */:
classStr = 'ANY';
break;
default:
classStr = `CLASS${baseClass}`;
}
return isFlush ? `${classStr}|FLUSH` : classStr;
}
/**
* Converts a DNS question class to a human-readable string.
* Adds support for mDNS QU (unicast-response) bit.
*
* @param {number} cls - The numeric question class.
* @returns {string} The string representation, e.g. "IN|QU"
*/
dnsQuestionClassToString(cls) {
const isQU = !!(cls & 32768 /* DnsClassFlag.QU */);
const baseClass = cls & 0x7fff;
let classStr;
switch (baseClass) {
case 1 /* DnsClass.IN */:
classStr = 'IN';
break;
case 3 /* DnsClass.CH */:
classStr = 'CH';
break;
case 4 /* DnsClass.HS */:
classStr = 'HS';
break;
case 255 /* DnsClass.ANY */:
classStr = 'ANY';
break;
default:
classStr = `CLASS${baseClass}`;
}
return isQU ? `${classStr}|QU` : classStr;
}
/**
* Logs the decoded mDNS message header.
*
* @param {MdnsMessage} msg - The mDNS message header object.
*/
logMdnsMessage(msg) {
this.log.info(`Decoded mDNS message: ID ${MAGENTA}${msg.id}${nf}, QR ${GREEN}${msg.qr === 0 ? 'Query' : 'Response'}${nf}, OPCODE ${MAGENTA}${msg.opcode}${nf}, AA ${MAGENTA}${msg.aa}${nf}, TC ${MAGENTA}${msg.tc}${nf}, RD ${MAGENTA}${msg.rd}${nf}, RA ${MAGENTA}${msg.ra}${nf}, Z ${MAGENTA}${msg.z}${nf}, RCODE ${MAGENTA}${msg.rcode}${nf}, QDCount ${MAGENTA}${msg.qdCount}${nf}, ANCount ${MAGENTA}${msg.anCount}${nf}, NSCount ${MAGENTA}${msg.nsCount}${nf}, ARCount ${MAGENTA}${msg.arCount}${nf}`);
msg.questions?.forEach((question) => {
this.log.info(`Question: ${CYAN}${question.name}${nf} type ${idn}${this.dnsTypeToString(question.type)}${rs}${nf} class ${CYAN}${this.dnsQuestionClassToString(question.class)}${nf}`);
});
msg.answers?.forEach((answer) => {
this.log.info(`Answer: ${CYAN}${answer.name}${nf} type ${idn}${this.dnsTypeToString(answer.type)}${rs}${nf} class ${CYAN}${this.dnsResponseClassToString(answer.class)}${nf} ttl ${CYAN}${answer.ttl}${nf} data ${CYAN}${answer.data}${nf}`);
});
msg.authorities?.forEach((authority) => {
this.log.info(`Authority: ${CYAN}${authority.name}${nf} type ${idn}${this.dnsTypeToString(authority.type)}${rs}${nf} class ${CYAN}${this.dnsResponseClassToString(authority.class)}${nf} ttl ${CYAN}${authority.ttl}${nf} data ${CYAN}${authority.data}${nf}`);
});
msg.additionals?.forEach((additional) => {
this.log.info(`Additional: ${CYAN}${additional.name}${nf} type ${idn}${this.dnsTypeToString(additional.type)}${rs}${nf} class ${CYAN}${this.dnsResponseClassToString(additional.class)}${nf} ttl ${CYAN}${additional.ttl}${nf} data ${CYAN}${additional.data}${nf}`);
});
this.log.info(`---\n`);
}
/**
* Logs the discovered devices from the mDNS queries and responses.
*/
logDevices() {
this.log.info(`Discovered query devices: ${MAGENTA}${this.deviceQueries.size}${nf}`);
// Collect devices into an array
const deviceQueryArray = Array.from(this.deviceQueries.entries());
// Sort the array by numeric value of the IP address
deviceQueryArray.sort(([addressA], [addressB]) => {
const partsA = addressA.split('.').map(Number);
const partsB = addressB.split('.').map(Number);
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const diff = (partsA[i] || 0) - (partsB[i] || 0);
if (diff !== 0)
return diff;
}
// istanbul ignore next
return 0;
});
// Log the sorted devices
deviceQueryArray.forEach(([rinfo, response]) => {
this.log.info(`- ${MAGENTA}${rinfo}${nf} family ${BLUE}${response.rinfo.family}${nf} address ${BLUE}${response.rinfo.address}${nf} port ${BLUE}${response.rinfo.port}${nf}`);
});
this.log.info(`Discovered response devices: ${MAGENTA}${this.deviceResponses.size}${nf}`);
// Collect devices into an array
const deviceResponseArray = Array.from(this.deviceResponses.entries());
// Sort the array by numeric value of the IP address
deviceResponseArray.sort(([addressA], [addressB]) => {
const partsA = addressA.split(/[:.]/).map((part) => parseInt(part, 16));
const partsB = addressB.split(/[:.]/).map((part) => parseInt(part, 16));
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const diff = (partsA[i] || 0) - (partsB[i] || 0);
if (diff !== 0)
return diff;
}
// istanbul ignore next
return 0;
});
// Log the sorted devices
deviceResponseArray.forEach(([rinfo, response]) => {
this.log.info(`- ${MAGENTA}${rinfo}${nf} family ${BLUE}${response.rinfo.family}${nf} address ${BLUE}${response.rinfo.address}${nf} port ${BLUE}${response.rinfo.port}${nf} PTR ${GREEN}${response.dataPTR}${nf}`);
});
}
}
//# sourceMappingURL=mdns.js.map