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

View file

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

View file

@ -3,8 +3,9 @@ const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const { log, genSecret, badgeConstants } = require("../src/util");
const passwordHash = require("./password-hash");
const { UDPClient, TCPClient, DOHClient } = require("dns2");
const { isIP, isIPv4, isIPv6 } = require("node:net");
const dnsPacket = require("dns-packet");
const dgram = require("dgram");
const { Socket, isIP, isIPv4, isIPv6 } = require("net");
const { Address6 } = require("ip-address");
const iconv = require("iconv-lite");
const chardet = require("chardet");
@ -21,6 +22,8 @@ const radiusClient = require("node-radius-client");
const redis = require("redis");
const oidc = require("openid-client");
const tls = require("tls");
const https = require("https");
const url = require("url");
const {
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
let resolver;
const isSecure = [ "DOH", "DOT" ].includes(transport.toUpperCase());
switch (transport.toUpperCase()) {
case "TCP":
resolver = TCPClient({
dns: resolverServer,
protocol: "tcp:",
port: resolverPort,
case "DOT": {
const buf = dnsPacket.streamEncode(requestData);
if (isSecure) {
const options = {
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);
});
} else {
client = new Socket();
client.connect(resolverPort, resolverServer, () => {
log.debug("dns", `Connected to ${resolverServer}:${resolverPort}`);
client.write(buf);
});
}
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;
case "DOT":
resolver = TCPClient({
dns: resolverServer,
protocol: "tls:",
port: resolverPort,
});
break;
case "DOH":
}
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}";
resolver = DOHClient({
dns: `https://${resolverServer}:${resolverPort}/${dohQuery}`,
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;
default:
resolver = UDPClient({
dns: resolverServer,
port: resolverPort,
socketType: "udp" + String(addressFamily),
}
//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(hostname, rrtype);
return resolver;
};
/**

View file

@ -572,7 +572,7 @@
"deleteMaintenanceMsg": "Are you sure want to delete this maintenance?",
"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.",
"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",
"dnsTransportDescription": "Select the transport method for querying the DNS server.",
"dohQueryPathDescription": "Set the query path to use for DNS wireformat. Must contain",

View file

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