Replace dns2 module with dns-packet

This commit is contained in:
ekrekeler 2025-02-28 02:54:56 -06:00
parent 21f629e055
commit 9c344ad371
No known key found for this signature in database
GPG key ID: 4C66C864B6B00854
5 changed files with 158 additions and 72 deletions

View file

@ -85,7 +85,7 @@
"croner": "~8.1.0", "croner": "~8.1.0",
"dayjs": "~1.11.5", "dayjs": "~1.11.5",
"dev-null": "^0.1.1", "dev-null": "^0.1.1",
"dns2": "song940/node-dns", "dns-packet": "~5.6.1",
"dotenv": "~16.0.3", "dotenv": "~16.0.3",
"express": "~4.21.0", "express": "~4.21.0",
"express-basic-auth": "~1.2.1", "express-basic-auth": "~1.2.1",
@ -170,6 +170,7 @@
"cronstrue": "~2.24.0", "cronstrue": "~2.24.0",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"delay": "^5.0.0", "delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~3.1.7", "dompurify": "~3.1.7",
"eslint": "~8.14.0", "eslint": "~8.14.0",
"eslint-plugin-jsdoc": "~46.4.6", "eslint-plugin-jsdoc": "~46.4.6",

View file

@ -25,77 +25,49 @@ class DnsMonitorType extends MonitorType {
let dnsMessage = ""; let dnsMessage = "";
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type, monitor.dns_transport, monitor.doh_query_path); let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type, monitor.dns_transport, monitor.doh_query_path);
const records = dnsRes.answers.map(record => {
return Buffer.isBuffer(record.data) ? record.data.toString() : record.data;
});
heartbeat.ping = dayjs().valueOf() - startTime; heartbeat.ping = dayjs().valueOf() - startTime;
const conditions = ConditionExpressionGroup.fromMonitor(monitor); const conditions = ConditionExpressionGroup.fromMonitor(monitor);
let conditionsResult = true; let conditionsResult = true;
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true; const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
let records = [];
switch (monitor.dns_resolve_type) { switch (monitor.dns_resolve_type) {
case "A": case "A":
case "AAAA": case "AAAA":
records = dnsRes.answers.map(record => {
switch (record.type) {
case 1: // A
case 28: // AAAA
return record.address;
case 5: // CNAME
return record.domain;
}
});
dnsMessage = `Records: ${records.join(" | ")}`;
conditionsResult = records.some(record => handleConditions({ record }));
break;
case "PTR":
records = dnsRes.answers.map(record => record.domain);
dnsMessage = `Records: ${records.join(" | ")}`;
conditionsResult = records.some(record => handleConditions({ record }));
break;
case "TXT": case "TXT":
records = dnsRes.answers.map(record => record.data); case "PTR":
dnsMessage = `Records: ${records.join(" | ")}`; case "NS":
dnsMessage = records.join(" | ");
conditionsResult = records.some(record => handleConditions({ record })); conditionsResult = records.some(record => handleConditions({ record }));
break; break;
case "CNAME": case "CNAME":
records.push(dnsRes.answers[0].domain);
dnsMessage = records[0]; dnsMessage = records[0];
conditionsResult = handleConditions({ record: records[0] }); conditionsResult = handleConditions({ record: records[0] });
break; break;
case "CAA": case "CAA":
// dns2 library currently has not implemented decoding CAA response dnsMessage = records.map(record => `${record.flags} ${record.tag} "${record.value}"`).join(" | ");
//records.push(dnsRes.answers[0].issue);
records.push("CAA issue placeholder");
dnsMessage = records[0];
conditionsResult = handleConditions({ record: records[0] }); conditionsResult = handleConditions({ record: records[0] });
break; break;
case "MX": case "MX":
records = dnsRes.answers.map(record => record.exchange); dnsMessage = records.map(record => `Hostname: ${record.exchange}; Priority: ${record.priority}`).join(" | ");
dnsMessage = dnsRes.answers.map(record => `Hostname: ${record.exchange} ; Priority: ${record.priority}`).join(" | ");
conditionsResult = records.some(record => handleConditions({ record }));
break;
case "NS":
records = dnsRes.answers.map(record => record.ns);
dnsMessage = `Servers: ${records.join(" | ")}`;
conditionsResult = records.some(record => handleConditions({ record })); conditionsResult = records.some(record => handleConditions({ record }));
break; break;
case "SOA": { case "SOA": {
records.push(dnsRes.answers[0].primary);
dnsMessage = Object.entries({ dnsMessage = Object.entries({
"Primary-NS": dnsRes.answers[0].primary, "Primary-NS": records[0].mname,
"Hostmaster": dnsRes.answers[0].admin, "Hostmaster": records[0].rname,
"Serial": dnsRes.answers[0].serial, "Serial": records[0].serial,
"Refresh": dnsRes.answers[0].refresh, "Refresh": records[0].refresh,
"Retry": dnsRes.answers[0].retry, "Retry": records[0].retry,
"Expire": dnsRes.answers[0].expiration, "Expire": records[0].expire,
"MinTTL": dnsRes.answers[0].minimum, "MinTTL": records[0].minimum,
}).map(([ name, value ]) => { }).map(([ name, value ]) => {
return `${name}: ${value}`; return `${name}: ${value}`;
}).join("; "); }).join("; ");
@ -104,8 +76,7 @@ class DnsMonitorType extends MonitorType {
} }
case "SRV": case "SRV":
records = dnsRes.answers.map(record => record.target); dnsMessage = records.map((record) => {
dnsMessage = dnsRes.answers.map((record) => {
return Object.entries({ return Object.entries({
"Target": record.target, "Target": record.target,
"Port": record.port, "Port": record.port,

View file

@ -3,8 +3,9 @@ const ping = require("@louislam/ping");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { log, genSecret, badgeConstants } = require("../src/util"); const { log, genSecret, badgeConstants } = require("../src/util");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
const { UDPClient, TCPClient, DOHClient } = require("dns2"); const dnsPacket = require("dns-packet");
const { isIP, isIPv4, isIPv6 } = require("node:net"); const dgram = require("dgram");
const { Socket, isIP, isIPv4, isIPv6 } = require("net");
const { Address6 } = require("ip-address"); const { Address6 } = require("ip-address");
const iconv = require("iconv-lite"); const iconv = require("iconv-lite");
const chardet = require("chardet"); const chardet = require("chardet");
@ -21,6 +22,8 @@ const radiusClient = require("node-radius-client");
const redis = require("redis"); const redis = require("redis");
const oidc = require("openid-client"); const oidc = require("openid-client");
const tls = require("tls"); const tls = require("tls");
const https = require("https");
const url = require("url");
const { const {
dictionaries: { dictionaries: {
@ -314,38 +317,149 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype, t
} }
} }
// This is the DNS request data that will get encoded later
const requestData = {
type: "query",
id: Math.floor(Math.random() * 65534) + 1,
flags: dnsPacket.RECURSION_DESIRED,
questions: [{
type: rrtype,
name: hostname,
}],
};
let client;
let resolver = null;
// Transport method determines which client type to use // Transport method determines which client type to use
let resolver; const isSecure = [ "DOH", "DOT" ].includes(transport.toUpperCase());
switch (transport.toUpperCase()) { switch (transport.toUpperCase()) {
case "TCP": case "TCP":
resolver = TCPClient({ case "DOT": {
dns: resolverServer, const buf = dnsPacket.streamEncode(requestData);
protocol: "tcp:", if (isSecure) {
const options = {
port: resolverPort, port: resolverPort,
host: resolverServer,
// TODO: Option for relaxing certificate validation
secureContext: tls.createSecureContext({
secureProtocol: "TLSv1_2_method",
}),
};
// TODO: Error handling for untrusted or expired cert
client = tls.connect(options, () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
client.write(buf);
}); });
break; } else {
case "DOT": client = new Socket();
resolver = TCPClient({ client.connect(resolverPort, resolverServer, () => {
dns: resolverServer, log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
protocol: "tls:", client.write(buf);
port: resolverPort,
});
break;
case "DOH":
dohQuery = dohQuery || "dns-query?dns={query}";
resolver = DOHClient({
dns: `https://${resolverServer}:${resolverPort}/${dohQuery}`,
});
break;
default:
resolver = UDPClient({
dns: resolverServer,
port: resolverPort,
socketType: "udp" + String(addressFamily),
}); });
} }
client.on("close", () => {
log.debug("dns", "Connection closed");
});
resolver = new Promise((resolve, reject) => {
let data = Buffer.alloc(0);
let expectedLength = 0;
client.on("data", (chunk) => {
if (data.length === 0) {
if (chunk.byteLength > 1) {
const plen = chunk.readUInt16BE(0);
expectedLength = plen;
if (plen < 12) {
reject("Response received is below DNS minimum packet length");
}
}
}
data = Buffer.concat([ data, chunk ]);
if (data.byteLength >= expectedLength) {
client.destroy();
const response = dnsPacket.streamDecode(data);
log.debug("dns", "Response decoded");
resolve(response);
}
});
});
break;
}
return resolver(hostname, rrtype); case "DOH": {
// Set query ID to "0" for HTTP cache friendlyness. See
// https://github.com/mafintosh/dns-packet/issues/77
requestData.id = 0;
const buf = dnsPacket.encode(requestData);
// TODO: implement POST requests for wireformat and JSON
dohQuery = dohQuery || "dns-query?dns={query}";
dohQuery = dohQuery.replace("{query}", buf.toString("base64url"));
const requestURL = url.parse(`https://${resolverServer}:${resolverPort}/${dohQuery}`, true);
const options = {
hostname: requestURL.hostname,
port: requestURL.port,
path: requestURL.path,
method: "GET",
headers: {
"Content-Type": "application/dns-message",
},
// TODO: Option for relaxing certificate validation
};
resolver = new Promise((resolve, reject) => {
client = https.request(options, (response) => {
let data = Buffer.alloc(0);
response.on("data", (chunk) => {
data = Buffer.concat([ data, chunk ]);
});
response.on("end", () => {
const response = dnsPacket.decode(data);
log.debug("dns", "Response decoded");
resolve(response);
});
});
client.on("socket", (socket) => {
socket.on("secureConnect", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
});
});
client.on("error", (err) => {
reject(err);
});
client.on("close", () => {
log.debug("dns", "Connection closed");
});
client.write(buf);
client.end();
});
break;
}
//case "UDP":
default: {
const buf = dnsPacket.encode(requestData);
client = dgram.createSocket("udp" + String(addressFamily));
client.on("connect", () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
});
client.on("close", () => {
log.debug("dns", "Connection closed");
});
resolver = new Promise((resolve, reject) => {
client.on("message", (rdata, rinfo) => {
client.close();
const response = dnsPacket.decode(rdata);
log.debug("dns", "Response decoded");
resolve(response);
});
client.on("error", (err) => {
reject(err);
});
});
client.send(buf, 0, buf.length, resolverPort, resolverServer);
}
}
return resolver;
}; };
/** /**

View file

@ -572,7 +572,7 @@
"deleteMaintenanceMsg": "Are you sure want to delete this maintenance?", "deleteMaintenanceMsg": "Are you sure want to delete this maintenance?",
"deleteNotificationMsg": "Are you sure want to delete this notification for all monitors?", "deleteNotificationMsg": "Are you sure want to delete this notification for all monitors?",
"dnsPortDescription": "DNS server port. Defaults to 53. Alternative ports are 443 for DoH and 853 for DoT.", "dnsPortDescription": "DNS server port. Defaults to 53. Alternative ports are 443 for DoH and 853 for DoT.",
"resolverserverDescription": "Cloudflare is the default server. You can change the resolver server anytime.", "resolverserverDescription": "Cloudflare is the default server. Use IP address for UDP/TCP/DoT, and domain name for DoH/DoT.",
"rrtypeDescription": "Select the RR type you want to monitor", "rrtypeDescription": "Select the RR type you want to monitor",
"dnsTransportDescription": "Select the transport method for querying the DNS server.", "dnsTransportDescription": "Select the transport method for querying the DNS server.",
"dohQueryPathDescription": "Set the query path to use for DNS wireformat. Must contain", "dohQueryPathDescription": "Set the query path to use for DNS wireformat. Must contain",

View file

@ -1225,10 +1225,10 @@ export default {
case "UDP": case "UDP":
case "TCP": case "TCP":
return this.ipRegexPattern.source; return this.ipRegexPattern.source;
case "DoH": case "DoH":
case "DoT":
return this.hostnameRegexPattern.source; return this.hostnameRegexPattern.source;
case "DoT":
return this.ipOrHostnameRegexPattern.source;
} }
} }
return null; return null;