From 5d9a570c77167ee4e6c5b0c52c4225f46cae77c8 Mon Sep 17 00:00:00 2001 From: PoleTransformer <78999213+PoleTransformer@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:11:49 +0000 Subject: [PATCH] feat: DNS monitor multi IP address and hostname support for Resolver Servers (#6524) Co-authored-by: PoleTransformer Co-authored-by: Frank Elsinga Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- server/monitor-types/dns.js | 84 +++++++++++++++++++++++++++++++++++-- server/util-server.js | 36 ---------------- src/lang/en.json | 4 +- src/pages/EditMonitor.vue | 14 +------ 4 files changed, 85 insertions(+), 53 deletions(-) diff --git a/server/monitor-types/dns.js b/server/monitor-types/dns.js index 1565ff1f2..8ee482a07 100644 --- a/server/monitor-types/dns.js +++ b/server/monitor-types/dns.js @@ -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 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 = { diff --git a/server/util-server.js b/server/util-server.js index 7587b0a84..e0ecf69fa 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -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 diff --git a/src/lang/en.json b/src/lang/en.json index b85718559..14bf701b4 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -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.", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index e40cf9bc1..8aae28a90 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -466,8 +466,8 @@