Merge pull request #238 from Ponkhy/dns-monitor

Added DNS Monitor Type
This commit is contained in:
Louis Lam 2021-08-28 00:21:10 +08:00 committed by GitHub
commit 7652b4849a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 302 additions and 3 deletions

10
db/patch7.sql Normal file
View File

@ -0,0 +1,10 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD dns_resolve_type VARCHAR(5);
ALTER TABLE monitor
ADD dns_resolve_server VARCHAR(255);
COMMIT;

144
extra/simple-dns-server.js Normal file
View File

@ -0,0 +1,144 @@
/*
* Simple DNS Server
* For testing DNS monitoring type, dev only
*/
const dns2 = require("dns2");
const { Packet } = dns2;
const server = dns2.createServer({
udp: true
});
server.on("request", (request, send, rinfo) => {
for (let question of request.questions) {
console.log(question.name, type(question.type), question.class);
const response = Packet.createResponseFromRequest(request);
if (question.name === "existing.com") {
if (question.type === Packet.TYPE.A) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
address: "1.2.3.4"
});
} if (question.type === Packet.TYPE.AAAA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
address: "fe80::::1234:5678:abcd:ef00",
});
} else if (question.type === Packet.TYPE.CNAME) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
domain: "cname1.existing.com",
});
} else if (question.type === Packet.TYPE.MX) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
exchange: "mx1.existing.com",
priority: 5
});
} else if (question.type === Packet.TYPE.NS) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
ns: "ns1.existing.com",
});
} else if (question.type === Packet.TYPE.SOA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
primary: "existing.com",
admin: "admin@existing.com",
serial: 2021082701,
refresh: 300,
retry: 3,
expiration: 10,
minimum: 10,
});
} else if (question.type === Packet.TYPE.SRV) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
priority: 5,
weight: 5,
port: 8080,
target: "srv1.existing.com",
});
} else if (question.type === Packet.TYPE.TXT) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
data: "#v=spf1 include:_spf.existing.com ~all",
});
} else if (question.type === Packet.TYPE.CAA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
flags: 0,
tag: "issue",
value: "ca.existing.com",
});
}
}
if (question.name === "4.3.2.1.in-addr.arpa") {
if (question.type === Packet.TYPE.PTR) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
domain: "ptr1.existing.com",
});
}
}
send(response);
}
});
server.on("listening", () => {
console.log("Listening");
console.log(server.addresses());
});
server.on("close", () => {
console.log("server closed");
});
server.listen({
udp: 5300
});
function type(code) {
for (let name in Packet.TYPE) {
if (Packet.TYPE[name] === code) {
return name;
}
}
}

13
package-lock.json generated
View File

@ -53,6 +53,7 @@
"@vitejs/plugin-vue": "^1.4.0", "@vitejs/plugin-vue": "^1.4.0",
"@vue/compiler-sfc": "^3.2.2", "@vue/compiler-sfc": "^3.2.2",
"core-js": "^3.16.1", "core-js": "^3.16.1",
"dns2": "^2.0.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-vue": "^7.16.0", "eslint-plugin-vue": "^7.16.0",
"sass": "^1.37.5", "sass": "^1.37.5",
@ -2457,6 +2458,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dns2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/dns2/-/dns2-2.0.1.tgz",
"integrity": "sha512-jHRTCcS2h/MEQjhcCnOWGENtz5A4RrLoK1YFqlHCejGfK5zYu99C8cxVwTsIY7JUqolhDN8zuGlyqnbEe6azqg==",
"dev": true
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -9441,6 +9448,12 @@
"path-type": "^4.0.0" "path-type": "^4.0.0"
} }
}, },
"dns2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/dns2/-/dns2-2.0.1.tgz",
"integrity": "sha512-jHRTCcS2h/MEQjhcCnOWGENtz5A4RrLoK1YFqlHCejGfK5zYu99C8cxVwTsIY7JUqolhDN8zuGlyqnbEe6azqg==",
"dev": true
},
"doctrine": { "doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",

View File

@ -32,7 +32,8 @@
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .", "test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .", "test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .", "test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile ." "test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
@ -79,6 +80,7 @@
"@vitejs/plugin-vue": "^1.4.0", "@vitejs/plugin-vue": "^1.4.0",
"@vue/compiler-sfc": "^3.2.2", "@vue/compiler-sfc": "^3.2.2",
"core-js": "^3.16.1", "core-js": "^3.16.1",
"dns2": "^2.0.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-vue": "^7.16.0", "eslint-plugin-vue": "^7.16.0",
"sass": "^1.37.5", "sass": "^1.37.5",

View File

@ -6,7 +6,7 @@ class Database {
static templatePath = "./db/kuma.db" static templatePath = "./db/kuma.db"
static path = "./data/kuma.db"; static path = "./data/kuma.db";
static latestVersion = 6; static latestVersion = 7;
static noReject = true; static noReject = true;
static sqliteInstance = null; static sqliteInstance = null;

View File

@ -7,7 +7,7 @@ dayjs.extend(timezone)
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification") const { Notification } = require("../notification")
@ -48,6 +48,8 @@ class Monitor extends BeanModel {
upsideDown: this.isUpsideDown(), upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects, maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(), accepted_statuscodes: this.getAcceptedStatuscodes(),
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
notificationIDList, notificationIDList,
}; };
} }
@ -174,6 +176,39 @@ class Monitor extends BeanModel {
bean.ping = await ping(this.hostname); bean.ping = await ping(this.hostname);
bean.msg = "" bean.msg = ""
bean.status = UP; bean.status = UP;
} else if (this.type === "dns") {
let startTime = dayjs().valueOf();
let dnsMessage = "";
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
bean.ping = dayjs().valueOf() - startTime;
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
dnsMessage += "Records: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
dnsMessage = dnsRes[0];
} else if (this.dns_resolve_type == "CAA") {
dnsMessage = dnsRes[0].issue;
} else if (this.dns_resolve_type == "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
} else if (this.dns_resolve_type == "NS") {
dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "SOA") {
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
} else if (this.dns_resolve_type == "SRV") {
dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
}
bean.msg = dnsMessage;
bean.status = UP;
} }
if (this.isUpsideDown()) { if (this.isUpsideDown()) {

View File

@ -291,6 +291,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.upsideDown = monitor.upsideDown; bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects; bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); 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;
await R.store(bean) await R.store(bean)

View File

@ -4,6 +4,7 @@ const { R } = require("redbean-node");
const { debug } = require("../src/util"); const { debug } = require("../src/util");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { Resolver } = require("dns");
/** /**
* Init or reset JWT secret * Init or reset JWT secret
@ -76,6 +77,30 @@ exports.pingAsync = function (hostname, ipv6 = false) {
}); });
} }
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
const resolver = new Resolver();
resolver.setServers([resolver_server]);
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);
}
});
}
})
}
exports.setting = async function (key) { exports.setting = async function (key) {
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key, key,

View File

@ -23,6 +23,9 @@
<option value="keyword"> <option value="keyword">
HTTP(s) - {{ $t("Keyword") }} HTTP(s) - {{ $t("Keyword") }}
</option> </option>
<option value="dns">
DNS
</option>
</select> </select>
</div> </div>
@ -54,6 +57,41 @@
<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">
</div> </div>
<div v-if="monitor.type === 'dns'" class="my-3">
<label for="hostname" class="form-label">Hostname</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'dns'" class="my-3">
<label for="dns_resolve_server" class="form-label">Resolver Server</label>
<input id="dns_resolve_server" v-model="monitor.dns_resolve_server" type="text" class="form-control" :pattern="ipRegex" required>
<div class="form-text">
Cloudflare is the default server, you can change the resolver server anytime.
</div>
</div>
<div v-if="monitor.type === 'dns'" class="my-3">
<label for="dns_resolve_type" class="form-label">Resource Record Type</label>
<VueMultiselect
id="dns_resolve_type"
v-model="monitor.dns_resolve_type"
:options="dnsresolvetypeOptions"
:multiple="false"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="false"
placeholder="Pick a RR-Type..."
:preselect-first="false"
:max-height="500"
:taggable="false"
></VueMultiselect>
<div class="form-text">
Select the RR-Type you want to monitor
</div>
</div>
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
@ -155,6 +193,7 @@
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect" import VueMultiselect from "vue-multiselect"
import { isDev } from "../util.ts";
const toast = useToast() const toast = useToast()
export default { export default {
@ -170,10 +209,24 @@ export default {
notificationIDList: {}, notificationIDList: {},
}, },
acceptedStatusCodeOptions: [], acceptedStatusCodeOptions: [],
dnsresolvetypeOptions: [],
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
ipRegexPattern: "((^\s*((([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]))\s*$)|(^\s*((([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}))|:)))(%.+)?\s*$))",
} }
}, },
computed: { computed: {
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)
if (! isDev) {
return this.ipRegexPattern;
}
return null;
},
pageName() { pageName() {
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit"); return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
}, },
@ -200,11 +253,25 @@ export default {
"500-599", "500-599",
]; ];
let dnsresolvetypeOptions = [
"A",
"AAAA",
"CAA",
"CNAME",
"MX",
"NS",
"PTR",
"SOA",
"SRV",
"TXT",
];
for (let i = 100; i <= 999; i++) { for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString()); acceptedStatusCodeOptions.push(i.toString());
} }
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions; this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
}, },
methods: { methods: {
init() { init() {
@ -221,6 +288,7 @@ export default {
upsideDown: false, upsideDown: false,
maxredirects: 10, maxredirects: 10,
accepted_statuscodes: ["200-299"], accepted_statuscodes: ["200-299"],
dns_resolve_server: "1.1.1.1",
} }
} else if (this.isEdit) { } else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {