@homebridge/ciao
Version:
ciao is a RFC 6763 compliant dns-sd library, advertising on multicast dns (RFC 6762) implemented in plain Typescript/JavaScript
572 lines • 26.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MDNSServer = void 0;
exports.SendResultFailedRatio = SendResultFailedRatio;
exports.SendResultFormatError = SendResultFormatError;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const dgram_1 = tslib_1.__importDefault(require("dgram"));
const DNSPacket_1 = require("./coder/DNSPacket");
const NetworkManager_1 = require("./NetworkManager");
const domain_formatter_1 = require("./util/domain-formatter");
const errors_1 = require("./util/errors");
const promise_utils_1 = require("./util/promise-utils");
const debug = (0, debug_1.default)("ciao:MDNSServer");
/**
* Returns the ration of rejected SendResults in the array.
* A ratio of 0 indicates all sends were successful.
* A ration of 1 indicates all sends failed.
* A number in between signals that some of the sends failed.
*
* @param results - Array of {@link SendResult}
*/
function SendResultFailedRatio(results) {
if (results.length === 0) {
return 0;
}
let failedCount = 0;
for (const result of results) {
if (result.status !== "fulfilled") {
failedCount++;
}
}
return failedCount / results.length;
}
function SendResultFormatError(results, prefix, includeStack = false) {
let failedCount = 0;
for (const result of results) {
if (result.status !== "fulfilled") {
failedCount++;
}
}
if (!prefix) {
prefix = "Failed to send packets";
}
if (failedCount < results.length) {
prefix += ` (${failedCount}/${results.length}):`;
}
else {
prefix += ":";
}
if (includeStack) {
let string = "=============================\n" + prefix;
for (const result of results) {
if (result.status === "rejected") {
string += "\n--------------------\n" +
"Failed to send packet on interface " + result.interface + ": " + result.reason.stack;
}
else if (result.status === "timeout") {
string += "\n--------------------\n" +
"Sending packet on interface " + result.interface + " timed out!";
}
}
string += "\n=============================";
return string;
}
else {
let string = prefix;
for (const result of results) {
if (result.status === "rejected") {
string += "\n- Failed to send packet on interface " + result.interface + ": " + result.reason.message;
}
else if (result.status === "timeout") {
string += "\n- Sending packet on interface " + result.interface + " timed out!";
}
}
return string;
}
}
/**
* This class can be used to create a mdns server to send and receive mdns packets on the local network.
*
* Currently only udp4 sockets will be advertised.
*/
class MDNSServer {
constructor(handler, options) {
this.sockets = new Map();
this.sentPackets = new Map();
// RFC 6762 15.1. If we are not the first responder bound to 5353 we can't receive unicast responses
// thus the QU flag must not be used in queries. Responders are only affected when sending probe queries.
// Probe queries should be sent with QU set, though can't be sent with QU when we can't receive unicast responses.
this.suppressUnicastResponseFlag = false;
this.bound = false;
this.closed = false;
this.advertiseFamilies = [];
(0, assert_1.default)(handler, "handler cannot be undefined");
this.handler = handler;
this.networkManager = new NetworkManager_1.NetworkManager({
interface: options && options.interface,
excludeIpv6: options && options.disableIpv6,
excludeIpv6Only: options && options.excludeIpv6Only,
});
if (!(options && options.advertiseIpv4 === false)) {
// IPv4 advertisements default to on
this.advertiseFamilies.push("IPv4" /* IPFamily.IPv4 */);
}
if (options && options.advertiseIpv6) {
// IPv6 advertisements default to off
this.advertiseFamilies.push("IPv6" /* IPFamily.IPv6 */);
}
this.networkManager.on("network-update" /* NetworkManagerEvent.NETWORK_UPDATE */, this.handleUpdatedNetworkInterfaces.bind(this));
}
getNetworkManager() {
return this.networkManager;
}
getBoundInterfaceNames() {
return this.sockets.keys();
}
async bind() {
if (this.closed) {
throw new Error("Cannot rebind closed server!");
}
// RFC 6762 15.1. suggest that we probe if we are not the only socket.
// though as ciao will probably always be installed besides an existing mdns responder, we just assume that without probing
// As it only affects probe queries, impact isn't that big.
this.suppressUnicastResponseFlag = true;
// wait for the first network interfaces to be discovered
await this.networkManager.waitForInit();
const promises = [];
for (const [name, networkInterface] of this.networkManager.getInterfaceMap()) {
this.advertiseFamilies.forEach((family) => {
const socket = this.createDgramSocket(name, true, family === "IPv6" /* IPFamily.IPv6 */ ? "udp6" : "udp4");
const promise = this.bindSocket(socket, networkInterface, family)
.catch(reason => {
// TODO if bind errors we probably will never bind again
console.log("Could not bind detected network interface: " + reason.stack);
});
promises.push(promise);
});
}
return Promise.all(promises).then(() => {
this.bound = true;
// map void[] to void
});
}
shutdown() {
this.networkManager.shutdown();
for (const socket of this.sockets.values()) {
socket.close();
}
this.bound = false;
this.closed = true;
this.sockets.clear();
}
sendQueryBroadcast(query, service) {
const packets = DNSPacket_1.DNSPacket.createDNSQueryPackets(query);
if (packets.length > 1) {
debug("Query broadcast is split into %d packets!", packets.length);
}
const promises = [];
for (const packet of packets) {
promises.push(this.sendOnAllNetworksForService(packet, service));
}
return Promise.all(promises).then((values) => {
const results = [];
for (const value of values) { // replace with .flat method when we have node >= 11.0.0 requirement
results.concat(value);
}
return results;
});
}
sendResponseBroadcast(response, service) {
const packet = DNSPacket_1.DNSPacket.createDNSResponsePacketsFromRRSet(response);
return this.sendOnAllNetworksForService(packet, service);
}
sendResponse(response, endpointOrInterface, callback) {
this.send(response, endpointOrInterface).then(result => {
if (result.status === "rejected") {
if (callback) {
callback(new Error("Encountered socket error on " + result.reason.name + ": " + result.reason.message));
}
else {
MDNSServer.logSocketError(result.interface, result.reason);
}
}
else if (callback) {
callback();
}
});
}
sendOnAllNetworksForService(packet, service) {
this.checkUnicastResponseFlag(packet);
const message = packet.encode();
this.assertBeforeSend(message, "IPv4" /* IPFamily.IPv4 */);
const promises = [];
for (const [name, socket] of this.sockets) {
if (!service.advertisesOnInterface(name)) {
// I don't like the fact that we put the check inside the MDNSServer, as it should be independent of the above layer.
// Though I think this is currently the easiest approach.
continue;
}
const isIPv6 = name.endsWith("/6");
const promise = new Promise(resolve => {
socket.send(message, MDNSServer.MDNS_PORT, isIPv6 ? MDNSServer.MULTICAST_IPV6 : MDNSServer.MULTICAST_IPV4, error => {
if (error) {
if (!MDNSServer.isSilencedSocketError(error)) {
resolve({
status: "rejected",
interface: name,
reason: error,
});
return;
}
}
else {
this.maintainSentPacketsInterface(name, message);
}
resolve({
status: "fulfilled",
interface: name,
});
});
});
promises.push(Promise.race([
promise,
(0, promise_utils_1.PromiseTimeout)(MDNSServer.SEND_TIMEOUT).then(() => ({
status: "timeout",
interface: name,
})),
]));
}
return Promise.all(promises);
}
send(packet, endpointOrInterface) {
this.checkUnicastResponseFlag(packet);
const message = packet.encode();
let address;
let port;
let name;
let isIPv6;
if (typeof endpointOrInterface === "string") { // its a network interface name
isIPv6 = endpointOrInterface.endsWith("/6");
address = isIPv6 ? MDNSServer.MULTICAST_IPV6 : MDNSServer.MULTICAST_IPV4;
port = MDNSServer.MDNS_PORT;
name = endpointOrInterface;
}
else {
isIPv6 = endpointOrInterface.interface.endsWith("/6");
address = endpointOrInterface.address;
port = endpointOrInterface.port;
name = endpointOrInterface.interface;
}
this.assertBeforeSend(message, isIPv6 ? "IPv6" /* IPFamily.IPv6 */ : "IPv4" /* IPFamily.IPv4 */);
const socket = this.sockets.get(name);
if (!socket) {
throw new errors_1.InterfaceNotFoundError(`Could not find socket for given network interface '${name}'`);
}
return new Promise(resolve => {
socket.send(message, port, address, error => {
if (error) {
if (!MDNSServer.isSilencedSocketError(error)) {
resolve({
status: "rejected",
interface: name,
reason: error,
});
return;
}
}
else {
this.maintainSentPacketsInterface(name, message);
}
resolve({
status: "fulfilled",
interface: name,
});
});
});
}
checkUnicastResponseFlag(packet) {
if (this.suppressUnicastResponseFlag && packet.type === 0 /* PacketType.QUERY */) {
packet.questions.forEach(record => record.unicastResponseFlag = false);
}
}
assertBeforeSend(message, family) {
if (this.closed) {
throw new errors_1.ServerClosedError("Cannot send packets on a closed mdns server!");
}
(0, assert_1.default)(this.bound, "Cannot send packets before server is not bound!");
const ipHeaderSize = family === "IPv4" /* IPFamily.IPv4 */ ? MDNSServer.DEFAULT_IP4_HEADER : MDNSServer.DEFAULT_IP6_HEADER;
// RFC 6762 17.
(0, assert_1.default)(ipHeaderSize + MDNSServer.UDP_HEADER + message.length <= 9000, "DNS cannot exceed the size of 9000 bytes even with IP Fragmentation!");
}
maintainSentPacketsInterface(name, packet) {
const base64 = packet.toString("base64");
const packets = this.sentPackets.get(name);
if (!packets) {
this.sentPackets.set(name, [base64]);
}
else {
packets.push(base64);
}
}
checkIfPacketWasPreviouslySentFromUs(name, packet) {
const base64 = packet.toString("base64");
const packets = this.sentPackets.get(name);
if (packets) {
const index = packets.indexOf(base64);
if (index !== -1) {
packets.splice(index, 1);
return true;
}
}
return false;
}
createDgramSocket(name, reuseAddr = false, type = "udp4") {
const socket = dgram_1.default.createSocket({
type: type,
reuseAddr: reuseAddr,
});
socket.on("message", (data, rinfo) => this.handleMessage(name, data, rinfo, type === "udp6" ? "IPv6" /* IPFamily.IPv6 */ : "IPv4" /* IPFamily.IPv4 */));
socket.on("error", error => {
if (!MDNSServer.isSilencedSocketError(error)) {
MDNSServer.logSocketError(name, error);
}
});
return socket;
}
bindSocket(socket, networkInterface, family) {
return new Promise((resolve, reject) => {
const errorHandler = (error) => reject(new Error("Failed to bind on interface " + networkInterface.name + ": " + error.message));
const isIPv6 = family === "IPv6" /* IPFamily.IPv6 */;
socket.once("error", errorHandler);
socket.on("close", () => {
this.sockets.delete(networkInterface.name + (isIPv6 ? "/6" : ""));
});
socket.bind(MDNSServer.MDNS_PORT, () => {
socket.setRecvBufferSize(800 * 1024); // setting max recv buffer size to 800KiB (Pi will max out at 352KiB)
socket.removeListener("error", errorHandler);
const multicastAddress = isIPv6 ? MDNSServer.MULTICAST_IPV6 : MDNSServer.MULTICAST_IPV4;
const interfaceAddress = isIPv6 ? networkInterface.ipv6 : networkInterface.ipv4;
// assert(interfaceAddress, "Interface address for " + networkInterface.name + " cannot be undefined!");
if (!interfaceAddress) {
// There isn't necessarily an IPv4 and IPv6 address assigned to every interface even on dual-stack systems
console.log("Warning: no " + (isIPv6 ? "IPv6" : "IPv4") + " address available on " + networkInterface.name);
try {
socket.close();
}
catch (error) {
// Ignore
}
resolve();
return;
}
try {
socket.addMembership(multicastAddress, interfaceAddress);
// socket.setMulticastInterface(isIPv6 ? "::%" + networkInterface.name : interfaceAddress!);
socket.setMulticastInterface(isIPv6 ? interfaceAddress + "%" + networkInterface.name : interfaceAddress);
socket.setMulticastTTL(MDNSServer.MDNS_TTL); // outgoing multicast datagrams
socket.setTTL(MDNSServer.MDNS_TTL); // outgoing unicast datagrams
socket.setMulticastLoopback(true); // We can't disable multicast loopback, as otherwise queriers on the same host won't receive our packets
this.sockets.set(isIPv6 ? networkInterface.name + "/6" : networkInterface.name, socket);
resolve();
}
catch (error) {
try {
socket.close();
}
catch (error) {
debug("Error while closing socket which failed to bind. Error may be expected: " + error.message);
}
reject(new Error("Error binding socket on " + networkInterface.name + ": " + error.stack));
}
});
});
}
handleMessage(name, buffer, rinfo, family) {
if (!this.bound) {
return;
}
const networkInterface = this.networkManager.getInterface(name);
if (!networkInterface) {
debug("Received packet on non existing network interface: %s!", name);
return;
}
if (this.checkIfPacketWasPreviouslySentFromUs(networkInterface.name, buffer)) {
// multicastLoopback is enabled for every interface, meaning we would receive our own response
// packets here. Thus, we silence them. We can't disable multicast loopback, as otherwise
// queriers on the same host won't receive our packets
return;
}
// We have the following problem on linux based platforms:
// When setting up a socket like above (binding on 0.0.0.0:5353) and then adding membership for 224.0.0.251 for
// A CERTAIN! interface, we will nonetheless receive packets from ALL other interfaces even the loopback interfaces.
// This is not the case on platforms like e.g. macOS. There stuff is properly filtered, and we only receive packets
// for the given interface we specified in our membership.
// This has the problem, that when receiving packets from other interfaces, that we leak addresses which don't
// exist on the given interface. We can't do much about it, as in typically multiple subnet networks, it is valid
// that we receive a packet from an ip address which doesn't belong into the subnet of our given interface.
// What we can do at least, is the following two things:
// * if we are listening on the loopback interface, we filter out any traffic which doesn't belong to the loopback interface
// * if we receive a packet from the loopback interface, we filter those out as well.
// With that we at least ensure that the loopback address is never sent out to the network.
// This is what we do below:
const isIPv6 = family === "IPv6" /* IPFamily.IPv6 */;
if (isIPv6) {
if (networkInterface.loopback !== rinfo.address.includes("%lo")) {
debug("Received packet on a %s interface (%s) which is coming from a %s interface (%s)", networkInterface.loopback ? "loopback" : "non-loopback", name, rinfo.address.includes("%lo") ? "loopback" : "non-loopback", rinfo.address);
// return;
}
}
else {
const ip4Netaddress = (0, domain_formatter_1.getNetAddress)(rinfo.address, networkInterface.ip4Netmask);
if (networkInterface.loopback) {
if (ip4Netaddress !== networkInterface.ipv4Netaddress) {
return;
}
}
else if (this.networkManager.isLoopbackNetaddressV4(ip4Netaddress)) {
debug("Received packet on interface '%s' which is not coming from the same subnet: %o", name, { address: rinfo.address, netaddress: ip4Netaddress, interface: networkInterface.ipv4 });
return;
}
}
let packet;
try {
packet = DNSPacket_1.DNSPacket.decode(rinfo, buffer);
}
catch (error) {
debug("Received a malformed packet from %o on interface %s. This might or might not be a problem. " +
"Here is the received packet for debugging purposes '%s'. " +
"Packet decoding failed with %s", rinfo, name, buffer.toString("base64"), error.stack);
return;
}
if (packet.opcode !== 0 /* OpCode.QUERY */) {
// RFC 6762 18.3 we MUST ignore messages with opcodes other than zero (QUERY)
return;
}
if (packet.rcode !== 0 /* RCode.NoError */) {
// RFC 6762 18.3 we MUST ignore messages with response code other than zero (NoError)
return;
}
const endpoint = {
address: rinfo.address,
port: rinfo.port,
interface: name + (isIPv6 ? "/6" : ""),
};
if (packet.type === 0 /* PacketType.QUERY */) {
try {
this.handler.handleQuery(packet, endpoint);
}
catch (error) {
console.warn("Error occurred handling incoming (on " + name + ") dns query packet: " + error.stack);
}
}
else if (packet.type === 1 /* PacketType.RESPONSE */) {
if (rinfo.port !== MDNSServer.MDNS_PORT) {
// RFC 6762 6. Multicast DNS implementations MUST silently ignore any Multicast DNS responses
// they receive where the source UDP port is not 5353.
return;
}
try {
this.handler.handleResponse(packet, endpoint);
}
catch (error) {
console.warn("Error occurred handling incoming (on " + name + ") dns response packet: " + error.stack);
}
}
}
static isSilencedSocketError(error) {
// silence those errors
// they happen when the host is not reachable (EADDRNOTAVAIL for 224.0.0.251 or EHOSTDOWN for any unicast traffic)
// caused by yet undetected network changes.
// as we listen to 0.0.0.0 and the socket stays valid, this is not a problem
const silenced = error.message.includes("EADDRNOTAVAIL") || error.message.includes("EHOSTDOWN")
|| error.message.includes("ENETUNREACH") || error.message.includes("EHOSTUNREACH")
|| error.message.includes("EPERM") || error.message.includes("EINVAL");
if (silenced) {
debug("Silenced and ignored error (This is/should not be a problem, this message is only for informational purposes): " + error.message);
}
return silenced;
}
static logSocketError(name, error) {
console.warn(`Encountered MDNS socket error on socket '${name}' : ${error.stack}`);
return;
}
handleUpdatedNetworkInterfaces(networkUpdate) {
if (networkUpdate.removed) {
for (const networkInterface of networkUpdate.removed) {
// Handle IPv4
let socket = this.sockets.get(networkInterface.name);
this.sockets.delete(networkInterface.name);
if (socket) {
socket.close();
}
// Handle IPv6
socket = this.sockets.get(networkInterface.name + "/6");
this.sockets.delete(networkInterface.name + "/6");
if (socket) {
socket.close();
}
}
}
if (networkUpdate.changes) {
for (const change of networkUpdate.changes) {
// Handle IPv4
let socket = this.sockets.get(change.name);
if (!change.outdatedIpv4 && change.updatedIpv4) {
// this does currently not happen, as we exclude ipv6 only interfaces
// thus such a change would be happening through the ADDED array
assert_1.default.fail("Reached illegal state! IPv4 address changed from undefined to defined!");
}
else if (change.outdatedIpv4 && !change.updatedIpv4) {
// this does currently not happen, as we exclude ipv6 only interfaces
// thus such a change would be happening through the REMOVED array
assert_1.default.fail("Reached illegal state! IPV4 address change from defined to undefined!");
}
else if (socket && change.outdatedIpv4 && change.updatedIpv4) {
try {
socket.dropMembership(MDNSServer.MULTICAST_IPV4, change.outdatedIpv4);
}
catch (error) {
debug("Thrown unexpected error when dropping outdated address membership: " + error.message);
}
try {
socket.addMembership(MDNSServer.MULTICAST_IPV4, change.updatedIpv4);
}
catch (error) {
debug("Thrown unexpected error when adding new address membership: " + error.message);
}
socket.setMulticastInterface(change.updatedIpv4);
}
// Handle IPv6
socket = this.sockets.get(change.name + "/6");
if (socket && change.outdatedIpv6 && change.updatedIpv6) {
try {
socket.dropMembership(MDNSServer.MULTICAST_IPV6, change.outdatedIpv6);
}
catch (error) {
debug("Thrown unexpected error when dropping outdated address membership: " + error.message);
}
try {
socket.addMembership(MDNSServer.MULTICAST_IPV6, change.updatedIpv6);
}
catch (error) {
debug("Thrown unexpected error when adding new address membership: " + error.message);
}
}
}
}
if (networkUpdate.added) {
for (const networkInterface of networkUpdate.added) {
this.advertiseFamilies.forEach((family) => {
const socket = this.createDgramSocket(networkInterface.name, true, family === "IPv6" /* IPFamily.IPv6 */ ? "udp6" : "udp4");
this.bindSocket(socket, networkInterface, family).catch(reason => {
// TODO if bind errors we probably will never bind again
console.log("Could not bind detected network interface: " + reason.stack);
});
});
}
}
}
}
exports.MDNSServer = MDNSServer;
MDNSServer.DEFAULT_IP4_HEADER = 20;
MDNSServer.DEFAULT_IP6_HEADER = 40;
MDNSServer.UDP_HEADER = 8;
MDNSServer.MDNS_PORT = 5353;
MDNSServer.MDNS_TTL = 255;
MDNSServer.MULTICAST_IPV4 = "224.0.0.251";
MDNSServer.MULTICAST_IPV6 = "FF02::FB";
MDNSServer.SEND_TIMEOUT = 200; // milliseconds
//# sourceMappingURL=MDNSServer.js.map