Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions db/knex_init_db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions db/knex_migrations/2025-22-02-0000-dns-trasnsport.js
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 7 additions & 3 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions server/monitor-conditions/operators.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -314,5 +319,6 @@ module.exports = {
operatorMap,
defaultStringOperators,
defaultNumberOperators,
defaultArrayOperators,
ConditionOperator,
};
132 changes: 104 additions & 28 deletions server/monitor-types/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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;
};

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 (monitor.dns_resolve_type) {
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 }));
break;

case "NS":
dnsMessage = `Servers: ${dnsRes.join(" | ")}`;
conditionsResult = dnsRes.some(record => handleConditions({ record }));
dnsMessage = records.map(record => `Hostname: ${record.exchange}; Priority: ${record.priority}`).join(" | ");
conditionsResult = handleConditions({ hostnames: records.map(record => record.exchange) });
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 });
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;
}

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 }));
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.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
if (monitor.dnsLastResult !== dnsMessage && dnsMessage !== undefined && monitor.id !== undefined) {
await R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ dnsMessage, monitor.id ]);
}

Expand Down
8 changes: 6 additions & 2 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading