mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-07-18 23:34:04 +02:00
Merge 4f7c8ae3b4
into 9976ef94af
This commit is contained in:
commit
6ad1972415
17 changed files with 1130 additions and 96 deletions
|
@ -86,6 +86,10 @@ async function createTables() {
|
|||
table.text("accepted_statuscodes_json").notNullable().defaultTo("[\"200-299\"]");
|
||||
table.string("dns_resolve_type", 5);
|
||||
table.string("dns_resolve_server", 255);
|
||||
table.string("dns_transport", 3);
|
||||
table.boolean("doh_query_path", 255).defaultTo("dns-query");
|
||||
table.boolean("force_http2").notNullable().defaultTo(false);
|
||||
table.boolean("skip_remote_dnssec").notNullable().defaultTo(false);
|
||||
table.string("dns_last_result", 255);
|
||||
table.integer("retry_interval").notNullable().defaultTo(0);
|
||||
table.string("push_token", 20).defaultTo(null);
|
||||
|
|
18
db/knex_migrations/2025-22-02-0000-dns-trasnsport.js
Normal file
18
db/knex_migrations/2025-22-02-0000-dns-trasnsport.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.string("dns_transport", 3).notNullable().defaultTo("UDP");
|
||||
table.string("doh_query_path", 255).defaultTo("dns-query");
|
||||
table.boolean("force_http2").notNullable().defaultTo(false);
|
||||
table.boolean("skip_remote_dnssec").notNullable().defaultTo(false);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("dns_transport");
|
||||
table.dropColumn("doh_query_path");
|
||||
table.dropColumn("force_http2");
|
||||
table.dropColumn("skip_remote_dnssec");
|
||||
});
|
||||
};
|
43
package-lock.json
generated
43
package-lock.json
generated
|
@ -28,6 +28,7 @@
|
|||
"croner": "~8.1.0",
|
||||
"dayjs": "~1.11.5",
|
||||
"dev-null": "^0.1.1",
|
||||
"dns-packet": "~5.6.1",
|
||||
"dotenv": "~16.0.3",
|
||||
"express": "~4.21.0",
|
||||
"express-basic-auth": "~1.2.1",
|
||||
|
@ -41,6 +42,7 @@
|
|||
"http-proxy-agent": "~7.0.2",
|
||||
"https-proxy-agent": "~7.0.6",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"ip-address": "~10.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "^2.0.3",
|
||||
|
@ -2706,6 +2708,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@louislam/ping": {
|
||||
"version": "0.4.4-mod.1",
|
||||
"resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.4-mod.1.tgz",
|
||||
|
@ -7798,6 +7806,18 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dns-packet": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
|
||||
"integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@leichtgewicht/ip-codec": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dns2": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/dns2/-/dns2-2.0.5.tgz",
|
||||
|
@ -10541,14 +10561,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
|
@ -15533,6 +15549,19 @@
|
|||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/socks/node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
"croner": "~8.1.0",
|
||||
"dayjs": "~1.11.5",
|
||||
"dev-null": "^0.1.1",
|
||||
"dns-packet": "~5.6.1",
|
||||
"dotenv": "~16.0.3",
|
||||
"express": "~4.21.0",
|
||||
"express-basic-auth": "~1.2.1",
|
||||
|
@ -99,6 +100,7 @@
|
|||
"http-proxy-agent": "~7.0.2",
|
||||
"https-proxy-agent": "~7.0.6",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"ip-address": "~10.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "^2.0.3",
|
||||
|
|
|
@ -120,9 +120,13 @@ class Monitor extends BeanModel {
|
|||
packetSize: this.packetSize,
|
||||
maxredirects: this.maxredirects,
|
||||
accepted_statuscodes: this.getAcceptedStatuscodes(),
|
||||
dns_resolve_type: this.dns_resolve_type,
|
||||
dns_resolve_server: this.dns_resolve_server,
|
||||
dns_last_result: this.dns_last_result,
|
||||
dnsResolveType: this.dnsResolveType,
|
||||
dnsResolveServer: this.dnsResolveServer,
|
||||
dnsTransport: this.dnsTransport,
|
||||
dohQueryPath: this.dohQueryPath,
|
||||
forceHttp2: Boolean(this.forceHttp2),
|
||||
skipRemoteDnssec: Boolean(this.skipRemoteDnssec),
|
||||
dnsLastResult: this.dnsLastResult,
|
||||
docker_container: this.docker_container,
|
||||
docker_host: this.docker_host,
|
||||
proxyId: this.proxy_id,
|
||||
|
|
|
@ -296,6 +296,11 @@ const defaultNumberOperators = [
|
|||
operatorMap.get(OP_GTE)
|
||||
];
|
||||
|
||||
const defaultArrayOperators = [
|
||||
operatorMap.get(OP_CONTAINS),
|
||||
operatorMap.get(OP_NOT_CONTAINS)
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
OP_STR_EQUALS,
|
||||
OP_STR_NOT_EQUALS,
|
||||
|
@ -314,5 +319,6 @@ module.exports = {
|
|||
operatorMap,
|
||||
defaultStringOperators,
|
||||
defaultNumberOperators,
|
||||
defaultArrayOperators,
|
||||
ConditionOperator,
|
||||
};
|
||||
|
|
|
@ -4,7 +4,11 @@ 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 {
|
||||
defaultStringOperators,
|
||||
defaultNumberOperators,
|
||||
defaultArrayOperators
|
||||
} = require("../monitor-conditions/operators");
|
||||
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
|
||||
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
|
||||
|
||||
|
@ -14,68 +18,140 @@ class DnsMonitorType extends MonitorType {
|
|||
supportsConditions = true;
|
||||
|
||||
conditionVariables = [
|
||||
new ConditionVariable("record", defaultStringOperators ),
|
||||
// A, AAAA, NS
|
||||
new ConditionVariable("records", defaultArrayOperators),
|
||||
// PTR, CNAME
|
||||
new ConditionVariable("hostname", defaultStringOperators),
|
||||
// CAA
|
||||
new ConditionVariable("flags", defaultStringOperators),
|
||||
new ConditionVariable("tag", defaultStringOperators),
|
||||
// CAA, TXT
|
||||
new ConditionVariable("value", defaultStringOperators),
|
||||
// MX
|
||||
new ConditionVariable("hostnames", defaultArrayOperators),
|
||||
// SOA
|
||||
new ConditionVariable("mname", defaultStringOperators),
|
||||
new ConditionVariable("rname", defaultStringOperators),
|
||||
new ConditionVariable("serial", defaultStringOperators),
|
||||
new ConditionVariable("refresh", defaultNumberOperators),
|
||||
new ConditionVariable("retry", defaultNumberOperators),
|
||||
new ConditionVariable("minimum", defaultNumberOperators),
|
||||
// SRV
|
||||
new ConditionVariable("servers", defaultArrayOperators),
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let startTime = dayjs().valueOf();
|
||||
let dnsMessage = "";
|
||||
const requestData = {
|
||||
name: monitor.hostname,
|
||||
rrtype: monitor.dnsResolveType,
|
||||
dnssec: true, // Request DNSSEC information in the response
|
||||
dnssecCheckingDisabled: monitor.skipRemoteDnssec,
|
||||
};
|
||||
const transportData = {
|
||||
type: monitor.dnsTransport,
|
||||
timeout: monitor.timeout,
|
||||
ignoreCertErrors: monitor.ignoreTls,
|
||||
dohQueryPath: monitor.dohQueryPath,
|
||||
dohUsePost: monitor.method === "POST",
|
||||
dohUseHttp2: monitor.forceHttp2,
|
||||
};
|
||||
|
||||
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type);
|
||||
let startTime = dayjs().valueOf();
|
||||
let dnsRes = await dnsResolve(requestData, monitor.dnsResolveServer, monitor.port, transportData);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
let dnsMessage = "";
|
||||
let rrtype = monitor.dnsResolveType;
|
||||
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
|
||||
let conditionsResult = true;
|
||||
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
|
||||
const checkRecord = (results, record) => {
|
||||
// Omit records that are not the same as the requested rrtype
|
||||
if (record.type === monitor.dnsResolveType) {
|
||||
// Concat TXT records with values larger than 255 characters
|
||||
results.push(Array.isArray(record.data) ? record.data.join("") : record.data);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
switch (monitor.dns_resolve_type) {
|
||||
const records = dnsRes.answers.reduce(checkRecord, []);
|
||||
// Return down status if no records are provided
|
||||
if (records.length === 0) {
|
||||
// Some DNS servers place SOA record in the authorities section
|
||||
if (dnsRes.authorities.map(auth => auth.type).includes(monitor.dnsResolveType)) {
|
||||
records.push(...dnsRes.authorities.reduce(checkRecord, []));
|
||||
} else {
|
||||
rrtype = null;
|
||||
dnsMessage = "No records found";
|
||||
conditionsResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (rrtype) {
|
||||
case "A":
|
||||
case "AAAA":
|
||||
case "PTR":
|
||||
dnsMessage = `Records: ${dnsRes.join(" | ")}`;
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record }));
|
||||
case "NS":
|
||||
dnsMessage = records.join(" | ");
|
||||
conditionsResult = handleConditions({ records: records });
|
||||
break;
|
||||
|
||||
case "TXT":
|
||||
dnsMessage = `Records: ${dnsRes.join(" | ")}`;
|
||||
conditionsResult = dnsRes.flat().some(record => handleConditions({ record }));
|
||||
dnsMessage = records.join(" | ");
|
||||
// Combine records to enable string operators for conditions
|
||||
conditionsResult = handleConditions({ value: records.join("|") });
|
||||
break;
|
||||
|
||||
// While PTR can have multiple records in DNS, it's not recommended
|
||||
case "PTR":
|
||||
case "CNAME":
|
||||
dnsMessage = dnsRes[0];
|
||||
conditionsResult = handleConditions({ record: dnsRes[0] });
|
||||
dnsMessage = records[0];
|
||||
conditionsResult = handleConditions({ hostname: records[0].value });
|
||||
break;
|
||||
|
||||
case "CAA":
|
||||
dnsMessage = dnsRes[0].issue;
|
||||
conditionsResult = handleConditions({ record: dnsRes[0].issue });
|
||||
dnsMessage = `${records[0].flags} ${records[0].tag} "${records[0].value}"`;
|
||||
conditionsResult = handleConditions(records[0]);
|
||||
break;
|
||||
|
||||
case "MX":
|
||||
dnsMessage = dnsRes.map(record => `Hostname: ${record.exchange} - Priority: ${record.priority}`).join(" | ");
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record: record.exchange }));
|
||||
dnsMessage = records.map(record => `Hostname: ${record.exchange}; Priority: ${record.priority}`).join(" | ");
|
||||
conditionsResult = handleConditions({ hostnames: records.map(record => record.exchange) });
|
||||
break;
|
||||
|
||||
case "NS":
|
||||
dnsMessage = `Servers: ${dnsRes.join(" | ")}`;
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record }));
|
||||
break;
|
||||
|
||||
case "SOA":
|
||||
dnsMessage = `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||
conditionsResult = handleConditions({ record: dnsRes.nsname });
|
||||
break;
|
||||
|
||||
case "SRV":
|
||||
dnsMessage = dnsRes.map(record => `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight}`).join(" | ");
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record: record.name }));
|
||||
case "SOA": {
|
||||
dnsMessage = Object.entries({
|
||||
"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("; ");
|
||||
conditionsResult = handleConditions(records[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
|
||||
case "SRV":
|
||||
dnsMessage = records.map((record) => {
|
||||
return Object.entries({
|
||||
"Server": `${record.target}:${record.port}`,
|
||||
"Priority": record.priority,
|
||||
"Weight": record.weight,
|
||||
}).map(([ name, value ]) => {
|
||||
return `${name}: ${value}`;
|
||||
}).join("; ");
|
||||
}).join(" | ");
|
||||
conditionsResult = handleConditions({ servers: records.map(record => `${record.target}:${record.port}`) });
|
||||
break;
|
||||
}
|
||||
|
||||
if (monitor.dnsLastResult !== dnsMessage && dnsMessage !== undefined && monitor.id !== undefined) {
|
||||
await R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ dnsMessage, monitor.id ]);
|
||||
}
|
||||
|
||||
|
|
|
@ -825,8 +825,12 @@ let needSetup = false;
|
|||
bean.packetSize = monitor.packetSize;
|
||||
bean.maxredirects = monitor.maxredirects;
|
||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||
bean.dnsResolveType = monitor.dnsResolveType;
|
||||
bean.dnsResolveServer = monitor.dnsResolveServer;
|
||||
bean.dnsTransport = monitor.dnsTransport;
|
||||
bean.dohQueryPath = monitor.dohQueryPath;
|
||||
bean.forceHttp2 = monitor.forceHttp2;
|
||||
bean.skipRemoteDnssec = monitor.skipRemoteDnssec;
|
||||
bean.pushToken = monitor.pushToken;
|
||||
bean.docker_container = monitor.docker_container;
|
||||
bean.docker_host = monitor.docker_host;
|
||||
|
|
|
@ -7,7 +7,11 @@ const {
|
|||
PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT
|
||||
} = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { Resolver } = require("dns");
|
||||
const dnsPacket = require("dns-packet");
|
||||
const optioncodes = require("dns-packet/optioncodes.js");
|
||||
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");
|
||||
const chroma = require("chroma-js");
|
||||
|
@ -23,6 +27,19 @@ const radiusClient = require("node-radius-client");
|
|||
const redis = require("redis");
|
||||
const oidc = require("openid-client");
|
||||
const tls = require("tls");
|
||||
const https = require("https");
|
||||
const http2 = require("http2");
|
||||
const url = require("url");
|
||||
|
||||
const {
|
||||
HTTP2_HEADER_PATH,
|
||||
HTTP2_HEADER_METHOD,
|
||||
HTTP2_HEADER_AUTHORITY,
|
||||
HTTP2_HEADER_ACCEPT,
|
||||
HTTP2_HEADER_CONTENT_LENGTH,
|
||||
HTTP2_HEADER_CONTENT_TYPE,
|
||||
HTTP2_HEADER_STATUS,
|
||||
} = http2.constants;
|
||||
|
||||
const {
|
||||
dictionaries: {
|
||||
|
@ -314,39 +331,439 @@ exports.httpNtlm = function (options, ntlmOptions) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Encode DNS query packet data to a buffer. Adapted from
|
||||
* https://github.com/hildjj/dohdec/blob/v7.0.2/pkg/dohdec/lib/dnsUtils.js
|
||||
* @param {object} opts Options for the query.
|
||||
* @param {string} opts.name The name to look up.
|
||||
* @param {number} opts.id ID for the query. SHOULD be 0 for DOH.
|
||||
* @param {packet.RecordType} opts.rrtype The record type to look up.
|
||||
* @param {boolean} opts.dnssec Request DNSSec information?
|
||||
* @param {boolean} opts.dnssecCheckingDisabled Disable DNSSec validation?
|
||||
* @param {string} opts.ecsSubnet Subnet to use for ECS.
|
||||
* @param {number} opts.ecs Number of ECS bits. Defaults to 24 (IPv4) or 56
|
||||
* (IPv6).
|
||||
* @param {boolean} opts.stream Encode for streaming, with the packet prefixed
|
||||
* by a 2-byte big-endian integer of the number of bytes in the packet.
|
||||
* @param {number} opts.udpPayloadSize Set a custom UDP payload size (EDNS).
|
||||
* @returns {Buffer} The encoded packet.
|
||||
* @throws {TypeError} opts does not contain a name attribute.
|
||||
*/
|
||||
exports.makeDnsPacket = function (opts) {
|
||||
const PAD_SIZE = 128;
|
||||
|
||||
if (!opts?.name) {
|
||||
throw new TypeError("Name is required");
|
||||
}
|
||||
|
||||
/** @type {dnsPacket.OptAnswer} */
|
||||
const opt = {
|
||||
name: ".",
|
||||
type: "OPT",
|
||||
udpPayloadSize: opts.udpPayloadSize || 4096,
|
||||
extendedRcode: 0,
|
||||
flags: 0,
|
||||
flag_do: false, // Setting here has no effect
|
||||
ednsVersion: 0,
|
||||
options: [],
|
||||
};
|
||||
|
||||
/** @type {dnsPacket.Packet} */
|
||||
const dns = {
|
||||
type: "query",
|
||||
id: opts.id || 0,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [{
|
||||
type: opts.rrtype || "A",
|
||||
class: "IN",
|
||||
name: opts.name,
|
||||
}],
|
||||
additionals: [ opt ],
|
||||
};
|
||||
//assert(dns.flags !== undefined);
|
||||
if (opts.dnssec) {
|
||||
dns.flags |= dnsPacket.AUTHENTIC_DATA;
|
||||
opt.flags |= dnsPacket.DNSSEC_OK;
|
||||
}
|
||||
if (opts.dnssecCheckingDisabled) {
|
||||
dns.flags |= dnsPacket.CHECKING_DISABLED;
|
||||
}
|
||||
if (
|
||||
(opts.ecs != null) ||
|
||||
(opts.ecsSubnet && (isIP(opts.ecsSubnet) !== 0))
|
||||
) {
|
||||
// https://tools.ietf.org/html/rfc7871#section-11.1
|
||||
const prefix = (opts.ecsSubnet && isIPv4(opts.ecsSubnet)) ? 24 : 56;
|
||||
opt.options.push({
|
||||
code: optioncodes.toCode("CLIENT_SUBNET"),
|
||||
ip: opts.ecsSubnet || "0.0.0.0",
|
||||
sourcePrefixLength: (opts.ecs == null) ? prefix : opts.ecs,
|
||||
});
|
||||
}
|
||||
const unpadded = dnsPacket.encodingLength(dns);
|
||||
opt.options.push({
|
||||
code: optioncodes.toCode("PADDING"),
|
||||
// Next pad size, minus what we already have, minus another 4 bytes for
|
||||
// the option header
|
||||
length: (Math.ceil(unpadded / PAD_SIZE) * PAD_SIZE) - unpadded - 4,
|
||||
});
|
||||
if (opts.stream) {
|
||||
return dnsPacket.streamEncode(dns);
|
||||
}
|
||||
return dnsPacket.encode(dns);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decodes DNS packet response data with error handling
|
||||
* @param {Buffer} data DNS packet data to decode
|
||||
* @param {boolean} isStream If the data is encoded as a stream
|
||||
* @param {Function} callback function to call if error is encountered
|
||||
* Passes error object as a parameter to the function
|
||||
* @returns {dnsPacket.Packet} DNS packet data in an object
|
||||
*/
|
||||
exports.decodeDnsPacket = function (data, isStream = false, callback) {
|
||||
let decodedData;
|
||||
try {
|
||||
decodedData = isStream ? dnsPacket.streamDecode(data) : dnsPacket.decode(data);
|
||||
log.debug("dns", "Response decoded");
|
||||
// If the truncated bit is set, the answers section was too large
|
||||
if (decodedData.flag_tc) {
|
||||
callback({ message: "Response is truncated." });
|
||||
}
|
||||
} catch (err) {
|
||||
err.message = `Error decoding DNS response data: ${err.message}`;
|
||||
log.warn("dns", err.message);
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
return decodedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a given record using the specified DNS server
|
||||
* @param {string} hostname The hostname of the record to lookup
|
||||
* @param {object} opts Options for the query, used to generate DNS packet
|
||||
* @param {string} opts.name The name of the record to query
|
||||
* @param {string} opts.rrtype The resource record type
|
||||
* @param {number} opts.id Set a specific ID number to use on the query
|
||||
* @param {number} opts.udpPayloadSize Set a custom UDP payload size (EDNS).
|
||||
* Defaults to safe values, 1432 bytes (IPv4) or 1232 bytes (IPv6).
|
||||
* @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
|
||||
* @param {number} resolverPort Port the DNS server is listening on
|
||||
* @param {object} transport The transport method and options
|
||||
* @param {string} transport.type Transport method, default is UDP
|
||||
* @param {number} transport.timeout Timeout to use for queries
|
||||
* @param {boolean} transport.ignoreCertErrors Proceed with secure connections
|
||||
* even if the server presents an untrusted or expired certificate
|
||||
* @param {string} transport.dohQueryPath Query path to use for DoH requests
|
||||
* @param {boolean} transport.dohUsePost If true, DNS query will be sent using
|
||||
* HTTP POST method for DoH requests, otherwise use HTTP GET method
|
||||
* @param {boolean} transport.dohUseHttp2 If true, DNS query will be made with
|
||||
* HTTP/2 session for DOH requests, otherwise use HTTP/1.1
|
||||
* @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)
|
||||
exports.dnsResolve = function (opts, resolverServer, resolverPort, transport) {
|
||||
// Set transport variables to defaults if not defined
|
||||
const method = ("type" in transport) ? transport.type.toUpperCase() : "UDP";
|
||||
const isSecure = [ "DOH", "DOT", "DOQ" ].includes(method);
|
||||
const timeout = transport.timeout ?? 30000; // 30 seconds
|
||||
const skipCertCheck = transport.ignoreCertErrors ?? false;
|
||||
const dohQuery = transport.dohQueryPath ?? "dns-query";
|
||||
const dohUsePost = transport.dohUsePost ?? false;
|
||||
const dohUseHttp2 = transport.dohUseHttp2 ?? false;
|
||||
|
||||
// Parse IPv4 and IPv6 addresses to determine address family and
|
||||
// add square brackets to IPv6 addresses, following RFC 3986 syntax
|
||||
resolverServer = resolverServer.replace("[", "").replace("]", "");
|
||||
resolver.setServers([ `[${resolverServer}]:${resolverPort}` ]);
|
||||
const addressFamily = isIP(resolverServer);
|
||||
if (addressFamily === 6) {
|
||||
resolverServer = `[${resolverServer}]`;
|
||||
}
|
||||
|
||||
// If performing reverse (PTR) record lookup, ensure hostname
|
||||
// syntax follows RFC 1034 / RFC 3596
|
||||
if (opts.rrtype === "PTR") {
|
||||
if (isIPv4(opts.name)) {
|
||||
let octets = opts.name.split(".");
|
||||
octets.reverse();
|
||||
opts.name = octets.join(".") + ".in-addr.arpa";
|
||||
} else if (isIPv6(opts.name)) {
|
||||
let address = new Address6(opts.name);
|
||||
opts.name = address.reverseForm();
|
||||
}
|
||||
}
|
||||
|
||||
// Set request ID
|
||||
if (opts.id == null) {
|
||||
// Set query ID to "0" for HTTP cache friendlyness on DoH requests.
|
||||
// See https://github.com/mafintosh/dns-packet/issues/77
|
||||
opts.id = (method === "DOH") ? 0 : Math.floor(Math.random() * 65534) + 1;
|
||||
}
|
||||
|
||||
// Set UDP payload size to safe levels for transmission over 1500 MTU
|
||||
if (!opts.udpPayloadSize) {
|
||||
opts.udpPayloadSize = (addressFamily === 4) ? 1432 : 1232;
|
||||
}
|
||||
|
||||
// Enable stream encoding for TCP and DOT transport methods
|
||||
if ([ "TCP", "DOT" ].includes(method)) {
|
||||
opts.stream = true;
|
||||
}
|
||||
// Generate buffer with encoded DNS query
|
||||
const buf = exports.makeDnsPacket(opts);
|
||||
|
||||
let client;
|
||||
let resolver;
|
||||
const socketName = `${resolverServer}:${resolverPort}`;
|
||||
// Transport method determines which client type to use
|
||||
switch (method) {
|
||||
|
||||
case "TCP":
|
||||
case "DOT": {
|
||||
if (isSecure) {
|
||||
const options = {
|
||||
port: resolverPort,
|
||||
host: resolverServer,
|
||||
rejectUnauthorized: !skipCertCheck,
|
||||
secureContext: tls.createSecureContext({
|
||||
minVersion: "TLSv1.2",
|
||||
}),
|
||||
};
|
||||
// Set TLS ServerName only if server is not an IP address per
|
||||
// Section 3 of RFC 6066
|
||||
if (addressFamily === 0) {
|
||||
options.servername = resolverServer;
|
||||
}
|
||||
client = tls.connect(options, () => {
|
||||
log.debug("dns", `Connected to ${socketName}`);
|
||||
client.write(buf);
|
||||
});
|
||||
} else {
|
||||
client = new Socket();
|
||||
client.connect(resolverPort, resolverServer, () => {
|
||||
log.debug("dns", `Connected to ${socketName}`);
|
||||
client.write(buf);
|
||||
});
|
||||
}
|
||||
resolver = new Promise((resolve, reject) => {
|
||||
// The below message is used when the response received does
|
||||
// not follow Section 4.2.2 of RFC 1035
|
||||
const lenErrMsg = "Resolver returned invalid DNS response";
|
||||
let data = Buffer.alloc(0);
|
||||
let expectedLength = 0;
|
||||
let isValidLength = false;
|
||||
client.on("error", (err) => {
|
||||
if (err.code === "ETIMEDOUT") {
|
||||
err.message = `Connection to ${socketName} timed out`;
|
||||
} else if (err.code === "ECONNREFUSED") {
|
||||
err.message = `Connection to ${socketName} refused`;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
client.setTimeout(timeout, () => {
|
||||
client.destroy();
|
||||
reject({ message: `Request to ${socketName} timed out` });
|
||||
});
|
||||
client.on("data", (chunk) => {
|
||||
if (data.length === 0) {
|
||||
if (chunk.byteLength > 1) {
|
||||
expectedLength = chunk.readUInt16BE(0);
|
||||
if (expectedLength < 12) {
|
||||
reject({ message: lenErrMsg });
|
||||
}
|
||||
}
|
||||
}
|
||||
data = Buffer.concat([ data, chunk ]);
|
||||
if (data.byteLength - 2 === expectedLength) {
|
||||
isValidLength = true;
|
||||
client.destroy();
|
||||
const response = exports.decodeDnsPacket(data, true, reject);
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
client.on("close", () => {
|
||||
log.debug("dns", `Connection to ${socketName} closed`);
|
||||
if (!isValidLength) {
|
||||
reject({ message: lenErrMsg });
|
||||
}
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "DOH": {
|
||||
const queryPath = dohUsePost ? dohQuery : `${dohQuery}?dns=${buf.toString("base64url")}`;
|
||||
const requestURL = url.parse(`https://${socketName}/${queryPath}`, true);
|
||||
const mimeType = "application/dns-message";
|
||||
const options = {
|
||||
hostname: requestURL.hostname,
|
||||
port: requestURL.port,
|
||||
path: requestURL.path,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"accept": mimeType,
|
||||
},
|
||||
rejectUnauthorized: !skipCertCheck,
|
||||
};
|
||||
if (dohUsePost) {
|
||||
options.method = "POST";
|
||||
// Setting Content-Length header is required for some resolvers
|
||||
options.headers["content-length"] = buf.byteLength;
|
||||
options.headers["content-type"] = mimeType;
|
||||
}
|
||||
resolver = new Promise((resolve, reject) => {
|
||||
/**
|
||||
* Helper function to validate HTTP response
|
||||
* @param {IncomingMessage|ClientHttp2Stream} httpResponse
|
||||
* The response from https or http2 client
|
||||
* @param {object} http2Headers Response headers from http2
|
||||
* @returns {void}
|
||||
* @throws missing one or more headers for HTTP/2 response
|
||||
*/
|
||||
const handleResponse = (httpResponse, http2Headers) => {
|
||||
// Determine status code and content type
|
||||
let statusCode;
|
||||
let contentType;
|
||||
if (dohUseHttp2) {
|
||||
if (!http2Headers) {
|
||||
throw new Error("No headers passed for HTTP/2 response");
|
||||
}
|
||||
statusCode = http2Headers[HTTP2_HEADER_STATUS];
|
||||
contentType = http2Headers[HTTP2_HEADER_CONTENT_TYPE];
|
||||
} else {
|
||||
statusCode = httpResponse.statusCode;
|
||||
contentType = httpResponse.headers["content-type"];
|
||||
}
|
||||
// Validate response from resolver
|
||||
if (statusCode !== 200) {
|
||||
reject({ message: `Request to ${socketName} failed with status code ${statusCode}` });
|
||||
return;
|
||||
} else if (contentType !== mimeType) {
|
||||
reject({ message: `Response from ${socketName} Content-Type was "${contentType}", expected ${mimeType}` });
|
||||
return;
|
||||
}
|
||||
// Read the response body into a buffer
|
||||
let data = Buffer.alloc(0);
|
||||
httpResponse.on("data", (chunk) => {
|
||||
data = Buffer.concat([ data, chunk ]);
|
||||
});
|
||||
httpResponse.on("end", () => {
|
||||
const response = exports.decodeDnsPacket(data, false, reject);
|
||||
resolve(response);
|
||||
});
|
||||
};
|
||||
if (dohUseHttp2) {
|
||||
const headers = {};
|
||||
headers[HTTP2_HEADER_AUTHORITY] = options.hostname;
|
||||
headers[HTTP2_HEADER_PATH] = options.path;
|
||||
headers[HTTP2_HEADER_METHOD] = options.method;
|
||||
headers[HTTP2_HEADER_ACCEPT] = options.headers["accept"];
|
||||
if (dohUsePost) {
|
||||
headers[HTTP2_HEADER_CONTENT_LENGTH] = options.headers["content-length"];
|
||||
headers[HTTP2_HEADER_CONTENT_TYPE] = options.headers["content-type"];
|
||||
}
|
||||
client = http2.connect(`https://${options.hostname}:${options.port}`, {
|
||||
rejectUnauthorized: options.rejectUnauthorized,
|
||||
});
|
||||
client.setTimeout(timeout, () => {
|
||||
client.destroy();
|
||||
reject({ message: `Request to ${socketName} timed out` });
|
||||
});
|
||||
client.on("connect", () => {
|
||||
log.debug("dns", `Connected to ${socketName}`);
|
||||
});
|
||||
const req = client.request(headers);
|
||||
req.on("error", (err) => {
|
||||
if (err.cause.code === "ETIMEDOUT") {
|
||||
err = err.cause;
|
||||
err.message = `Connection to ${socketName} timed out`;
|
||||
} else if (err.cause.code === "ECONNREFUSED") {
|
||||
err = err.cause;
|
||||
err.message = `Connection to ${socketName} refused`;
|
||||
} else {
|
||||
err.message = "HTTP/2: " + err.message;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
req.on("response", (resHeaders) => {
|
||||
handleResponse(req, resHeaders);
|
||||
});
|
||||
req.on("end", () => {
|
||||
client.close();
|
||||
});
|
||||
if (dohUsePost) {
|
||||
req.write(buf);
|
||||
}
|
||||
req.end();
|
||||
} else {
|
||||
client = https.request(options, (httpResponse) => {
|
||||
handleResponse(httpResponse);
|
||||
});
|
||||
client.setTimeout(timeout, () => {
|
||||
client.destroy();
|
||||
reject({ message: `Request to ${socketName} timed out` });
|
||||
});
|
||||
client.on("socket", (socket) => {
|
||||
socket.on("secureConnect", () => {
|
||||
log.debug("dns", `Connected to ${socketName}`);
|
||||
});
|
||||
});
|
||||
if (dohUsePost) {
|
||||
client.write(buf);
|
||||
}
|
||||
client.end();
|
||||
}
|
||||
client.on("error", (err) => {
|
||||
if (err.code === "ETIMEDOUT") {
|
||||
err.message = `Connection to ${socketName} timed out`;
|
||||
} else if (err.code === "ECONNREFUSED") {
|
||||
err.message = `Connection to ${socketName} refused`;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
client.on("close", () => {
|
||||
log.debug("dns", `Connection to ${socketName} closed`);
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "UDP":
|
||||
default: {
|
||||
if (addressFamily === 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (rrtype === "PTR") {
|
||||
resolver.reverse(hostname, (err, records) => {
|
||||
if (err) {
|
||||
reject({ message: "Resolver server must be IP address for UDP transport method" });
|
||||
});
|
||||
}
|
||||
client = dgram.createSocket(`udp${addressFamily}`);
|
||||
resolver = new Promise((resolve, reject) => {
|
||||
let timer;
|
||||
client.on("message", (rdata, rinfo) => {
|
||||
client.close();
|
||||
const response = exports.decodeDnsPacket(rdata, false, reject);
|
||||
resolve(response);
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(records);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolver.resolve(hostname, rrtype, (err, records) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(records);
|
||||
}
|
||||
client.on("listening", () => {
|
||||
log.debug("dns", `Connected to ${socketName}`);
|
||||
timer = setTimeout(() => {
|
||||
reject({ message: `Query to ${socketName} timed out` });
|
||||
client.close();
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
client.on("close", () => {
|
||||
clearTimeout(timer);
|
||||
log.debug("dns", `Connection to ${socketName} closed`);
|
||||
});
|
||||
});
|
||||
client.send(buf, 0, buf.length, resolverPort, resolverServer);
|
||||
}
|
||||
}
|
||||
|
||||
return resolver;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -629,6 +629,10 @@ $shadow-box-padding: 20px;
|
|||
}
|
||||
}
|
||||
|
||||
#doh-method {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.toast-container {
|
||||
margin-bottom: 100px !important;
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
.multiselect__single {
|
||||
line-height: 14px;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
|
||||
"ignoredTLSError": "TLS/SSL errors have been ignored",
|
||||
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL errors for secure connections",
|
||||
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||
"Upside Down Mode": "Upside Down Mode",
|
||||
|
@ -144,7 +144,11 @@
|
|||
"Test": "Test",
|
||||
"Certificate Info": "Certificate Info",
|
||||
"Resolver Server": "Resolver Server",
|
||||
"Transport Method": "Transport Method",
|
||||
"Query Path": "Query Path",
|
||||
"Force HTTP2": "HTTP/2",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
"Skip Remote DNSSEC Verification": "Skip Remote DNSSEC Verification",
|
||||
"Last Result": "Last Result",
|
||||
"Create your admin account": "Create your admin account",
|
||||
"Repeat Password": "Repeat Password",
|
||||
|
@ -277,6 +281,7 @@
|
|||
"Shrink Database": "Shrink Database",
|
||||
"shrinkDatabaseDescriptionSqlite": "Trigger database {vacuum} for SQLite. {auto_vacuum} is already enabled but this does not defragment the database nor repack individual database pages the way that the {vacuum} command does.",
|
||||
"Pick a RR-Type...": "Pick a RR-Type…",
|
||||
"Select the transport method...": "Select the transport method…",
|
||||
"Pick Accepted Status Codes...": "Pick Accepted Status Codes…",
|
||||
"Default": "Default",
|
||||
"HTTP Options": "HTTP Options",
|
||||
|
@ -582,9 +587,14 @@
|
|||
"deleteMonitorMsg": "Are you sure want to delete this monitor?",
|
||||
"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.",
|
||||
"dnsPortDescription": "DNS server port. Defaults to 53. Alternative ports are 443 for DoH and 853 for DoT.",
|
||||
"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.",
|
||||
"dohHttpMethodDescription": "Set the HTTP method to use for DoH query.",
|
||||
"forceHttp2": "Send the request using HTTP/2. Fails if the server does not support HTTP/2.",
|
||||
"skipRemoteDnssecDescription": "Requests the resolver server not to perform DNSSEC verification against queried records.",
|
||||
"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.",
|
||||
"clearEventsMsg": "Are you sure want to delete all events for this monitor?",
|
||||
|
|
|
@ -32,9 +32,9 @@
|
|||
<br>
|
||||
<span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
|
||||
<span v-if="monitor.type === 'dns'">[{{ monitor.dnsResolveType }}] {{ monitor.hostname }}
|
||||
<br>
|
||||
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>
|
||||
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dnsLastResult }}</span>
|
||||
</span>
|
||||
<span v-if="monitor.type === 'docker'">Docker container: {{ monitor.docker_container }}</span>
|
||||
<span v-if="monitor.type === 'gamedig'">Gamedig - {{ monitor.game }}: {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
|
|
|
@ -307,7 +307,11 @@
|
|||
v-model="monitor.hostname"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`"
|
||||
:pattern="`${
|
||||
monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern.source :
|
||||
monitor.type === 'dns' ? ipOrDnsNameRegexPattern.source :
|
||||
ipOrHostnameRegexPattern.source
|
||||
}`"
|
||||
required
|
||||
data-testid="hostname-input"
|
||||
>
|
||||
|
@ -396,8 +400,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") }}</label>
|
||||
<input id="dns-resolve-server" ref="dns-resolve-server" v-model="monitor.dnsResolveServer" type="text" class="form-control" :pattern="dnsResolverRegex" required data-testid="resolve-server-input">
|
||||
<div class="form-text">
|
||||
{{ $t("resolverserverDescription") }}
|
||||
</div>
|
||||
|
@ -406,19 +410,19 @@
|
|||
<!-- Port -->
|
||||
<div class="my-3">
|
||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1" data-testid="port-input">
|
||||
<div class="form-text">
|
||||
{{ $t("dnsPortDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="dns_resolve_type" class="form-label">{{ $t("Resource Record Type") }}</label>
|
||||
<label for="dns-resolve-type" class="form-label">{{ $t("Resource Record Type") }}</label>
|
||||
|
||||
<!-- :allow-empty="false" is not working, set a default value instead https://github.com/shentao/vue-multiselect/issues/336 -->
|
||||
<VueMultiselect
|
||||
id="dns_resolve_type"
|
||||
v-model="monitor.dns_resolve_type"
|
||||
id="dns-resolve-type"
|
||||
v-model="monitor.dnsResolveType"
|
||||
:options="dnsresolvetypeOptions"
|
||||
:multiple="false"
|
||||
:close-on-select="true"
|
||||
|
@ -435,6 +439,27 @@
|
|||
{{ $t("rrtypeDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="dns-transport" class="form-label">{{ $t("Transport Method") }}</label>
|
||||
<VueMultiselect
|
||||
id="dns-transport"
|
||||
v-model="monitor.dnsTransport"
|
||||
:options="dnsTransportOptions"
|
||||
:multiple="false"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="false"
|
||||
:placeholder="$t('Select the transport method...')"
|
||||
:preselect-first="false"
|
||||
:max-height="500"
|
||||
:taggable="false"
|
||||
data-testid="transport-method-select"
|
||||
></VueMultiselect>
|
||||
<div class="form-text">
|
||||
{{ $t("dnsTransportDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Docker Container Name / ID -->
|
||||
|
@ -655,10 +680,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' " class="my-3 form-check">
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' || (monitor.type === 'dns' && isSecureDnsTransport)" class="my-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
{{ monitor.type === "redis" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
|
||||
{{ monitor.type === "redis" || monitor.type === "dns" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -674,6 +699,63 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced DNS monitor settings -->
|
||||
<div v-if="monitor.type === 'dns'" class="my-3">
|
||||
<div v-if="dohSelected">
|
||||
<div class="d-flex">
|
||||
<div class="my-3 flex-column flex-fill">
|
||||
<div>
|
||||
<label for="doh-method" class="form-label">{{ $t("Method") }}</label>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-inline-flex">
|
||||
<select id="doh-method" v-model="monitor.method" class="form-select" data-testid="method-select">
|
||||
<option value="GET">
|
||||
GET
|
||||
</option>
|
||||
<option value="POST">
|
||||
POST
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mx-3 flex-fill">
|
||||
<input :value="dohDisplayUrl" type="button" class="form-control text-center" @click="focusElement('dns-resolve-server')">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
{{ $t("dohHttpMethodDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-3 flex-column flex-fill">
|
||||
<label for="doh-query-path" class="form-label">{{ $t("Query Path") }}</label>
|
||||
<input id="doh-query-path" v-model="monitor.dohQueryPath" type="text" class="form-control" :pattern="urlQueryRegex" placeholder="dns-query">
|
||||
<div class="form-text">
|
||||
{{ $t("dohQueryPathDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-3 form-check">
|
||||
<input id="force-http2" v-model="monitor.forceHttp2" class="form-check-input" type="checkbox" data-testid="http2-check">
|
||||
<label class="form-check-label" for="force-http2">
|
||||
{{ $t("Force HTTP2") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("forceHttp2") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check">
|
||||
<input id="skip_remote_dnssec" v-model="monitor.skip_remote_dnssec" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="skip_remote_dnssec">
|
||||
{{ $t("Skip Remote DNSSEC Verification") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("skipRemoteDnssecDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
|
@ -1144,7 +1226,11 @@ import {
|
|||
MIN_INTERVAL_SECOND,
|
||||
sleep,
|
||||
} from "../util.ts";
|
||||
import { hostNameRegexPattern } from "../util-frontend";
|
||||
import {
|
||||
hostNameRegexPattern,
|
||||
dnsNameRegexPattern,
|
||||
urlPathRegexPattern
|
||||
} from "../util-frontend";
|
||||
import HiddenInput from "../components/HiddenInput.vue";
|
||||
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
|
||||
|
||||
|
@ -1169,8 +1255,12 @@ const monitorDefaults = {
|
|||
expiryNotification: false,
|
||||
maxredirects: 10,
|
||||
accepted_statuscodes: [ "200-299" ],
|
||||
dns_resolve_type: "A",
|
||||
dns_resolve_server: "1.1.1.1",
|
||||
dnsResolveType: "A",
|
||||
dnsResolveServer: "1.1.1.1",
|
||||
dnsTransport: "UDP",
|
||||
dohQueryPath: "dns-query",
|
||||
forceHttp2: false,
|
||||
skip_remote_dnssec: false,
|
||||
docker_container: "",
|
||||
docker_host: null,
|
||||
proxyId: null,
|
||||
|
@ -1223,9 +1313,14 @@ export default {
|
|||
},
|
||||
acceptedStatusCodeOptions: [],
|
||||
dnsresolvetypeOptions: [],
|
||||
dnsTransportOptions: [],
|
||||
kafkaSaslMechanismOptions: [],
|
||||
ipRegexPattern: hostNameRegexPattern(false, true, false),
|
||||
hostnameRegexPattern: hostNameRegexPattern(false, false, true),
|
||||
ipOrHostnameRegexPattern: hostNameRegexPattern(),
|
||||
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true),
|
||||
ipOrDnsNameRegexPattern: dnsNameRegexPattern(),
|
||||
queryRegexPattern: urlPathRegexPattern(),
|
||||
gameList: null,
|
||||
connectionStringTemplates: {
|
||||
"sqlserver": "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>",
|
||||
|
@ -1286,7 +1381,43 @@ export default {
|
|||
|
||||
// Allow to test with simple dns server with port (127.0.0.1:5300)
|
||||
if (! isDev) {
|
||||
return this.ipRegexPattern;
|
||||
return this.ipRegexPattern.source;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
ipOrHostnameRegex() {
|
||||
|
||||
// Permit either IP address or hostname (127.0.0.1, dns.example.com)
|
||||
if (! isDev) {
|
||||
return this.ipOrHostnameRegexPattern.source;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
dnsResolverRegex() {
|
||||
|
||||
// Permit IP address for TCP/UDP resolvers, hostname for DoH/DoT
|
||||
if (! isDev) {
|
||||
switch (this.monitor.dnsTransport) {
|
||||
case "UDP":
|
||||
case "TCP":
|
||||
return this.ipRegexPattern.source;
|
||||
case "DoH":
|
||||
return this.hostnameRegexPattern.source;
|
||||
case "DoT":
|
||||
case "DoQ":
|
||||
return this.ipOrHostnameRegexPattern.source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
urlQueryRegex() {
|
||||
|
||||
// Permit only URL paths with a query parameter ( {query} )
|
||||
if (! isDev) {
|
||||
return this.queryRegexPattern.source;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
@ -1516,8 +1647,48 @@ message HealthCheckResponse {
|
|||
},
|
||||
|
||||
conditionVariables() {
|
||||
// For DNS monitor, the variables depend on rrtype. Conditions for
|
||||
// all rrtypes are added to an array in the DnsMonitorType class in
|
||||
// order to pass to Vue, then identified based on index.
|
||||
if (this.monitor.type === "dns") {
|
||||
const dnsConditionVariables = this.$root.monitorTypeList["dns"]?.conditionVariables;
|
||||
switch (this.monitor.dnsResolveType) {
|
||||
case "A":
|
||||
case "AAAA":
|
||||
case "NS":
|
||||
return dnsConditionVariables.slice(0, 1);
|
||||
case "PTR":
|
||||
case "CNAME":
|
||||
return dnsConditionVariables.slice(1, 2);
|
||||
case "CAA":
|
||||
return dnsConditionVariables.slice(2, 5);
|
||||
case "TXT":
|
||||
return dnsConditionVariables.slice(4, 5);
|
||||
case "MX":
|
||||
return dnsConditionVariables.slice(5, 6);
|
||||
case "SOA":
|
||||
return dnsConditionVariables.slice(6, 12);
|
||||
case "SRV":
|
||||
return dnsConditionVariables.slice(12, 13);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return this.$root.monitorTypeList[this.monitor.type]?.conditionVariables || [];
|
||||
},
|
||||
|
||||
dohSelected() {
|
||||
return this.monitor.dnsTransport === "DoH";
|
||||
},
|
||||
|
||||
dohDisplayUrl() {
|
||||
const port = (this.monitor.port !== 443) ? `:${this.monitor.port}` : "";
|
||||
return `https://${this.monitor.dnsResolveServer}${port}/`;
|
||||
},
|
||||
|
||||
isSecureDnsTransport() {
|
||||
return [ "DoH", "DoT", "DoQ" ].includes(this.monitor.dnsTransport);
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
"$root.proxyList"() {
|
||||
|
@ -1686,6 +1857,13 @@ message HealthCheckResponse {
|
|||
"TXT",
|
||||
];
|
||||
|
||||
let dnsTransportOptions = [
|
||||
"UDP",
|
||||
"TCP",
|
||||
"DoH",
|
||||
"DoT",
|
||||
];
|
||||
|
||||
let kafkaSaslMechanismOptions = [
|
||||
"None",
|
||||
"plain",
|
||||
|
@ -1700,6 +1878,7 @@ message HealthCheckResponse {
|
|||
|
||||
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
|
||||
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
|
||||
this.dnsTransportOptions = dnsTransportOptions;
|
||||
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
|
||||
},
|
||||
methods: {
|
||||
|
@ -2045,6 +2224,11 @@ message HealthCheckResponse {
|
|||
}
|
||||
},
|
||||
|
||||
focusElement(refId) {
|
||||
// Focus the element that has a defined reference
|
||||
this.$refs[refId].focus();
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -109,20 +109,80 @@ export function getDevContainerServerHostname() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Regex pattern fr identifying hostnames and IP addresses
|
||||
* Regex pattern for identifying hostnames and IP addresses
|
||||
* @param {boolean} mqtt whether or not the regex should take into
|
||||
* account the fact that it is an mqtt uri
|
||||
* @param {boolean} ip whether the regex should match IP addresses
|
||||
* @param {boolean} hostname whether the regex should match hostnames
|
||||
* @returns {RegExp} The requested regex
|
||||
*/
|
||||
export function hostNameRegexPattern(mqtt = false) {
|
||||
export function hostNameRegexPattern(mqtt = false, ip = true, hostname = true) {
|
||||
// mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect)
|
||||
const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?";
|
||||
const mqttSchemeRegexPattern = /((mqtt|ws)s?:\/\/)?/;
|
||||
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
|
||||
const ipRegexPattern = `((^${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`;
|
||||
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
|
||||
const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])(\\.)?$`;
|
||||
const ipv4RegexPattern = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
|
||||
const ipv6RegexPattern = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
||||
// See this answer for detailed a explanation: https://stackoverflow.com/a/53875771/1854468
|
||||
/* eslint-disable-next-line no-useless-escape */
|
||||
const hostNameRegexPattern = /^[a-zA-Z][a-zA-Z0-9\-]{0,62}\.([a-zA-Z0-9][a-zA-Z0-9\-]{0,62}\.)*([a-zA-Z]{2,63}|[xX][nN]--[a-zA-Z0-9]{0,59})$/;
|
||||
/* eslint-disable-next-line no-useless-escape */
|
||||
const localNameRegexPattern = /^[a-zA-Z0-9][a-zA-Z0-9\-]{0,127}$/;
|
||||
|
||||
return `${ipRegexPattern}|${hostNameRegexPattern}`;
|
||||
let patterns = [];
|
||||
if (ip) {
|
||||
patterns.push(ipv4RegexPattern, ipv6RegexPattern);
|
||||
}
|
||||
if (hostname) {
|
||||
patterns.push(hostNameRegexPattern, localNameRegexPattern);
|
||||
}
|
||||
if (mqtt) {
|
||||
// all modified patterns must start with "^" for this to work
|
||||
patterns = patterns.map(pattern => {
|
||||
return new RegExp(`^${mqttSchemeRegexPattern.source}${pattern.source.slice(1)}`);
|
||||
});
|
||||
}
|
||||
|
||||
return new RegExp(patterns.map(pattern => `(${pattern.source})`).join("|"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex pattern for DNS queries
|
||||
* @returns {RegExp} The requested regex
|
||||
*/
|
||||
export function dnsNameRegexPattern() {
|
||||
// This borrows ipRegexPattern from hostNameRegexPattern above
|
||||
const ipRegexPattern = hostNameRegexPattern(false, true, false);
|
||||
// Similar to hostNameRegexPattern, except the hostname pattern
|
||||
// can also match root (.) and top-level domains (.com, .org),
|
||||
// and may contain underscores (_)
|
||||
const dnsNamePattern = /^(\.|(\.?[a-zA-Z0-9\-_]+)+)$/;
|
||||
|
||||
return new RegExp(`(${ipRegexPattern.source})|(${dnsNamePattern.source})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex patterns for validating URL paths
|
||||
* @param {boolean} qstr True if the url should contain a query string
|
||||
* @param {boolean} tmpl True if the url should contain templating, `{query}`
|
||||
* Takes precedence over qstr if both are true.
|
||||
* @returns {RegExp} The requested regex
|
||||
*/
|
||||
export function urlPathRegexPattern(qstr = false, tmpl = false) {
|
||||
// Matches any URL path, including empty string
|
||||
const pathRegexPattern = /^\/?(([a-zA-Z0-9\-_%])+\/)*[a-zA-Z0-9\-_%]*(\?([a-zA-Z0-9\-_%]+=[a-zA-Z0-9\-_%]*&?)+)?$/;
|
||||
// Ensures a URL path follows query string format
|
||||
const queryRegexPattern = /^\/?(([a-zA-Z0-9\-_%])+\/)*[a-zA-Z0-9\-_%]*\?([a-zA-Z0-9\-_%]+=[a-zA-Z0-9\-_%]*&?)+$/;
|
||||
// Only checks for valid URL path containing "{query}"
|
||||
/* eslint-disable-next-line no-useless-escape */
|
||||
const queryTemplateRegexPattern = /^[a-zA-Z0-9\-._~:\/?#\[\]@!$&'\(\)*+,;=]*\{query\}[a-zA-Z0-9\-._~:\/?#\[\]@!$&'\(\)*+,;=]*$/;
|
||||
|
||||
if (tmpl) {
|
||||
return queryTemplateRegexPattern;
|
||||
}
|
||||
if (qstr) {
|
||||
return queryRegexPattern;
|
||||
}
|
||||
return pathRegexPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
181
test/backend-test/test-dns.js
Normal file
181
test/backend-test/test-dns.js
Normal file
|
@ -0,0 +1,181 @@
|
|||
const { describe, test } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { DnsMonitorType } = require("../../server/monitor-types/dns");
|
||||
const { UP, PENDING } = require("../../src/util");
|
||||
|
||||
const queryName = ".";
|
||||
const rrtype = "NS";
|
||||
const sampleRecordRegex = /a\.root-servers\.net/;
|
||||
const bogusServer = "0.0.0.0";
|
||||
|
||||
/**
|
||||
* Performs DNS query and checks the result
|
||||
* @param {object} monitorOpts the parameters for the monitor
|
||||
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
|
||||
*/
|
||||
async function testDns(monitorOpts) {
|
||||
if (!monitorOpts.hostname) {
|
||||
monitorOpts.hostname = queryName;
|
||||
}
|
||||
if (!monitorOpts.dnsResolveType) {
|
||||
monitorOpts.dnsResolveType = rrtype;
|
||||
}
|
||||
if (!monitorOpts.conditions) {
|
||||
monitorOpts.conditions = "[]";
|
||||
}
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
const dnsMonitor = new DnsMonitorType();
|
||||
|
||||
await dnsMonitor.check(monitorOpts, heartbeat, {});
|
||||
|
||||
return heartbeat;
|
||||
}
|
||||
|
||||
describe("DNS monitor transport methods", {
|
||||
concurrency: true
|
||||
}, () => {
|
||||
test("DNS (UDP)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "1.1.1.1",
|
||||
port: 53,
|
||||
dnsTransport: "UDP",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS (UDP) timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 53,
|
||||
dnsTransport: "UDP",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Query to .* timed out/
|
||||
}, "Expected query timeout error");
|
||||
});
|
||||
|
||||
test("DNS (TCP)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "1.1.1.1",
|
||||
port: 53,
|
||||
dnsTransport: "TCP",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS (TCP) timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 53,
|
||||
dnsTransport: "TCP",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
test("DNS over TLS", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "one.one.one.one",
|
||||
port: 853,
|
||||
dnsTransport: "DoT",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS over TLS timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 853,
|
||||
dnsTransport: "DoT",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
test("DNS over HTTPS (GET)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "cloudflare-dns.com",
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
dohQueryPath: "dns-query",
|
||||
method: "GET",
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS over HTTPS timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
test("DNS over HTTP/2 (POST)", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: "cloudflare-dns.com",
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
dohQueryPath: "dns-query",
|
||||
method: "POST",
|
||||
forceHttp2: true,
|
||||
};
|
||||
|
||||
const heartbeat = await testDns(monitor);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.match(heartbeat.msg, sampleRecordRegex);
|
||||
});
|
||||
|
||||
test("DNS over HTTP/2 timeout", async () => {
|
||||
|
||||
const monitor = {
|
||||
dnsResolveServer: bogusServer,
|
||||
port: 443,
|
||||
dnsTransport: "DoH",
|
||||
timeout: 10000,
|
||||
forceHttp2: true,
|
||||
};
|
||||
|
||||
await assert.rejects(testDns(monitor), {
|
||||
message: /Connection to .* (timed out|refused)/
|
||||
}, "Expected connection timeout error");
|
||||
});
|
||||
|
||||
});
|
|
@ -53,7 +53,7 @@ test.describe("Monitor Form", () => {
|
|||
|
||||
const friendlyName = "Example DNS NS";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("example.com");
|
||||
await page.getByTestId("hostname-input").fill(".");
|
||||
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
|
@ -65,9 +65,9 @@ test.describe("Monitor Form", () => {
|
|||
await page.getByTestId("add-condition-button").click();
|
||||
expect(await page.getByTestId("condition").count()).toEqual(2); // 2 explicitly added
|
||||
|
||||
await page.getByTestId("condition-value").nth(0).fill("a.iana-servers.net");
|
||||
await page.getByTestId("condition-value").nth(0).fill("a.root-servers.net");
|
||||
await page.getByTestId("condition-and-or").nth(0).selectOption("or");
|
||||
await page.getByTestId("condition-value").nth(1).fill("b.iana-servers.net");
|
||||
await page.getByTestId("condition-value").nth(1).fill("b.root-servers.net");
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
await page.getByTestId("save-button").click();
|
||||
|
@ -86,7 +86,7 @@ test.describe("Monitor Form", () => {
|
|||
|
||||
const friendlyName = "Example DNS NS";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("example.com");
|
||||
await page.getByTestId("hostname-input").fill(".");
|
||||
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
|
@ -105,4 +105,38 @@ test.describe("Monitor Form", () => {
|
|||
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("dns transport", async ({ page }, testInfo) => {
|
||||
await page.goto("./add");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
await selectMonitorType(page);
|
||||
|
||||
const friendlyName = "Cloudflare";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("one.one.one.one");
|
||||
await page.getByTestId("resolve-server-input").fill("cloudflare-dns.com");
|
||||
await page.getByTestId("port-input").fill("443");
|
||||
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
await resolveTypeSelect.getByRole("option", { name: "SOA" }).click();
|
||||
|
||||
const transportMethodSelect = page.getByTestId("transport-method-select");
|
||||
await transportMethodSelect.click();
|
||||
await transportMethodSelect.getByRole("option", { name: "DoH" }).click();
|
||||
|
||||
const httpMethodSelect = page.getByTestId("method-select");
|
||||
await httpMethodSelect.selectOption("POST");
|
||||
|
||||
await page.getByTestId("http2-check").check();
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
await page.getByTestId("save-button").click();
|
||||
await page.waitForURL("/dashboard/*");
|
||||
|
||||
expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue