feat: DNS monitor multi IP address and hostname support for Resolver Servers (#6524)
Co-authored-by: PoleTransformer <you@example.com> Co-authored-by: Frank Elsinga <frank@elsinga.de> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
79b3274441
commit
5d9a570c77
@ -1,12 +1,13 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const { dnsResolve } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { ConditionVariable } = require("../monitor-conditions/variables");
|
||||
const { defaultStringOperators } = require("../monitor-conditions/operators");
|
||||
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
|
||||
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
|
||||
const { Resolver } = require("node:dns/promises");
|
||||
const net = require("node:net");
|
||||
|
||||
class DnsMonitorType extends MonitorType {
|
||||
name = "dns";
|
||||
@ -24,7 +25,8 @@ class DnsMonitorType extends MonitorType {
|
||||
let startTime = dayjs().valueOf();
|
||||
let dnsMessage = "";
|
||||
|
||||
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type);
|
||||
const resolverServers = await this.resolveDnsResolverServers(monitor.dns_resolve_server);
|
||||
let dnsRes = await this.dnsResolve(monitor.hostname, resolverServers, monitor.port, monitor.dns_resolve_type);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
|
||||
@ -88,6 +90,82 @@ class DnsMonitorType extends MonitorType {
|
||||
heartbeat.msg = dnsMessage;
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list of DNS resolver servers and resolves any hostnames
|
||||
* to their corresponding IPv4 and/or IPv6 addresses.
|
||||
*
|
||||
* We are primarily doing this to support hostnames of docker containers like adguard.
|
||||
*
|
||||
* - Whitespace is removed from the input string
|
||||
* - Empty entries are ignored
|
||||
* - IP literals (IPv4 / IPv6) are accepted as-is
|
||||
* - Hostnames are resolved to both A and AAAA records in parallel
|
||||
* - Invalid or unresolvable entries are logged and skipped
|
||||
* @param {string} dnsResolveServer - Comma-separated list of resolver servers (IPs or hostnames)
|
||||
* @returns {Promise<Array<string>>} Array of resolved IP addresses
|
||||
* @throws {Error} If no valid resolver servers could be parsed or resolved
|
||||
*/
|
||||
async resolveDnsResolverServers(dnsResolveServer) {
|
||||
// Remove all spaces, split into array, remove all elements that are empty
|
||||
const addresses = dnsResolveServer.replace(/\s/g, "").split(",").filter((x) => x !== "");
|
||||
if (!addresses.length) {
|
||||
throw new Error("No Resolver Servers specified. Please specifiy at least one resolver server like 1.1.1.1 or a hostname");
|
||||
}
|
||||
const resolver = new Resolver();
|
||||
|
||||
// Make promises to be resolved concurrently
|
||||
const promises = addresses.map(async (e) => {
|
||||
if (net.isIP(e)) { // If IPv4 or IPv6 addr, immediately return
|
||||
return [ e ];
|
||||
}
|
||||
|
||||
// Otherwise, attempt to resolve hostname
|
||||
const [ v4, v6 ] = await Promise.allSettled([
|
||||
resolver.resolve4(e),
|
||||
resolver.resolve6(e),
|
||||
]);
|
||||
|
||||
const addrs = [
|
||||
...(v4.status === "fulfilled" ? v4.value : []),
|
||||
...(v6.status === "fulfilled" ? v6.value : []),
|
||||
];
|
||||
|
||||
if (!addrs.length) {
|
||||
log.error("DNS", `Invalid resolver server ${e}`);
|
||||
}
|
||||
return addrs;
|
||||
});
|
||||
|
||||
// [[ips of hostname1],[ips hostname2],...]
|
||||
const ips = await Promise.all(promises);
|
||||
// Append all the ips in [[]] to a single []
|
||||
const parsed = ips.flat();
|
||||
|
||||
// only the resolver resolution can discard an address
|
||||
// -> no special error message for only the net.isIP case is necessary
|
||||
if (!parsed.length) {
|
||||
throw new Error("None of the configured resolver servers could be resolved to an IP address. Please provide a comma-separated list of valid resolver hostnames or IP addresses.");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a given record using the specified DNS server
|
||||
* @param {string} hostname The hostname of the record to lookup
|
||||
* @param {string[]} resolverServer Array of DNS server IP addresses to use
|
||||
* @param {string} resolverPort Port the DNS server is listening on
|
||||
* @param {string} rrtype The type of record to request
|
||||
* @returns {Promise<(string[] | object[] | object)>} DNS response
|
||||
*/
|
||||
async dnsResolve(hostname, resolverServer, resolverPort, rrtype) {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers(resolverServer.map(server => `[${server}]:${resolverPort}`));
|
||||
if (rrtype === "PTR") {
|
||||
return await resolver.reverse(hostname);
|
||||
}
|
||||
return await resolver.resolve(hostname, rrtype);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -6,7 +6,6 @@ const {
|
||||
PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT
|
||||
} = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { Resolver } = require("dns");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
const chroma = require("chroma-js");
|
||||
@ -285,41 +284,6 @@ exports.httpNtlm = function (options, ntlmOptions) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a given record using the specified DNS server
|
||||
* @param {string} hostname The hostname of the record to lookup
|
||||
* @param {string} resolverServer The DNS server to use
|
||||
* @param {string} resolverPort Port the DNS server is listening on
|
||||
* @param {string} rrtype The type of record to request
|
||||
* @returns {Promise<(string[] | object[] | object)>} DNS response
|
||||
*/
|
||||
exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
|
||||
const resolver = new Resolver();
|
||||
// Remove brackets from IPv6 addresses so we can re-add them to
|
||||
// prevent issues with ::1:5300 (::1 port 5300)
|
||||
resolverServer = resolverServer.replace("[", "").replace("]", "");
|
||||
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (rrtype === "PTR") {
|
||||
resolver.reverse(hostname, (err, records) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(records);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolver.resolve(hostname, rrtype, (err, records) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(records);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Query radius server
|
||||
* @param {string} hostname Hostname of radius server
|
||||
|
||||
@ -147,7 +147,7 @@
|
||||
"Email": "Email",
|
||||
"Test": "Test",
|
||||
"Certificate Info": "Certificate Info",
|
||||
"Resolver Server": "Resolver Server",
|
||||
"Resolver Server(s)": "Resolver Server(s)",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
"Last Result": "Last Result",
|
||||
"Create your admin account": "Create your admin account",
|
||||
@ -625,7 +625,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. You can change the port at any time.",
|
||||
"resolverserverDescription": "Cloudflare is the default server. You can change the resolver server anytime.",
|
||||
"resolverserverDescription": "Cloudflare is the default server. You can specify a comma delimited list of IP addresses or hostnames.",
|
||||
"rrtypeDescription": "Select the RR type you want to monitor",
|
||||
"pauseMonitorMsg": "Are you sure want to pause?",
|
||||
"enableDefaultNotificationDescription": "This notification will be enabled by default for new monitors. You can still disable the notification separately for each monitor.",
|
||||
|
||||
@ -466,8 +466,8 @@
|
||||
<!-- For DNS Type -->
|
||||
<template v-if="monitor.type === 'dns'">
|
||||
<div class="my-3">
|
||||
<label for="dns_resolve_server" class="form-label">{{ $t("Resolver Server") }}</label>
|
||||
<input id="dns_resolve_server" v-model="monitor.dns_resolve_server" type="text" class="form-control" :pattern="ipRegex" required>
|
||||
<label for="dns_resolve_server" class="form-label">{{ $t("Resolver Server(s)") }}</label>
|
||||
<input id="dns_resolve_server" v-model="monitor.dns_resolve_server" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
{{ $t("resolverserverDescription") }}
|
||||
</div>
|
||||
@ -1352,7 +1352,6 @@ import ProxyDialog from "../components/ProxyDialog.vue";
|
||||
import TagsManager from "../components/TagsManager.vue";
|
||||
import {
|
||||
genSecret,
|
||||
isDev,
|
||||
MAX_INTERVAL_SECOND,
|
||||
MIN_INTERVAL_SECOND,
|
||||
sleep,
|
||||
@ -1502,15 +1501,6 @@ export default {
|
||||
return this.$t("defaultFriendlyName");
|
||||
},
|
||||
|
||||
ipRegex() {
|
||||
|
||||
// Allow to test with simple dns server with port (127.0.0.1:5300)
|
||||
if (! isDev) {
|
||||
return this.ipRegexPattern;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
monitorTypeUrlHost() {
|
||||
const { type, url, hostname, grpcUrl } = this.monitor;
|
||||
return {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user