mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-07-19 15:44:02 +02:00
Replace dns2 module with dns-packet
This commit is contained in:
parent
21f629e055
commit
9c344ad371
5 changed files with 158 additions and 72 deletions
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue