UNPKG

matterbridge

Version:
760 lines • 37 kB
/** * @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