UNPKG

dns-setting

Version:

dns management: query, modify, shield corresponding domain name resolution

525 lines (515 loc) 13.6 kB
'use strict'; var dns = require('native-dns'); var node_net = require('node:net'); var s3 = require('sqlite3'); var fs = require('fs'); require('assert'); require('util'); var express = require('express'); const evmt$1 = process.env; const port = evmt$1.port || 53; const address = evmt$1.address || "127.0.0.1"; const webPage = evmt$1.webPage === "false" ? false : true; const httpPort = evmt$1.httpPort || 2048; const apiUpdate = evmt$1.apiUpdate === "true" ? true : false; console.log( "port:", port, "address:", address, "webPage:", webPage, "httpPort:", httpPort, "apiUpdate:", apiUpdate, "\n" ); let __dnsList = []; function dnsList(newValue) { if (Array.isArray(newValue)) __dnsList = newValue; return __dnsList; } const dbp = "./db"; const sqlite3 = s3.verbose(); const dbName = `${dbp}/dns.db`; function read$1(src) { try { return fs.readFileSync(src).toString(); } catch (error) { console.error("read file error", src); return null; } } function hook(fun) { const db = new sqlite3.Database(dbName); let r = new Promise((res, rej) => fun(db, res, rej)); db.close(); return r; } function serialize() { try { fs.rmSync(dbName); } catch (e) {} hook((db) => { db.serialize(() => { db.exec(read$1(`${dbp}/serialize.sql`)); }); }); } const evmt = process.env; if (evmt.serialize) { serialize(); } function getWhere(data, not = []) { let keyList = Object.keys(data); let result = {}; let where = keyList .flatMap((k) => { if (not.includes(k) || (data[k] ?? null) === null) { return []; } if (/Like$/.test(k)) { result[`\$${k}`] = "%" + data[k] + "%"; return `${k.replace(/Like$/, "")} like \$${k}`; } result[`\$${k}`] = data[k]; return `${k} = \$${k}`; }) .join(" and "); if (where) { where = `where ${where}`; } return { where, data: result }; } function getSet(data, not = []) { let keyList = Object.keys(data); let result = {}; let str = keyList .flatMap((k) => { if (not.includes(k) || (data[k] ?? null) === null) { return []; } result[`\$${k}`] = data[k]; return `${k} = \$${k}`; }) .join(" , "); if (str) { str = `SET ${str}`; } return { set: str, data: result }; } function all(query, data) { return hook((db, res, rej) => { db.all(query.trim(), data, function (error, rows) { if (error) rej(error); else res(rows); }); }); } function tableList(name, match) { if ((match.isUsing ?? null) !== null) match.isUsing = match.isUsing ? 1 : 0; let { where, data } = getWhere(match); let query = `SELECT * FROM ${name} ${where}`; return all(query, data); } function updateItem(name, id, info) { if (info.isUsing ?? null === null) info.isUsing = 1; info.isUsing = info.isUsing ? 1 : 0; let { set, data } = getSet({ ...info }); let query = `UPDATE ${name} ${set} where id = $id`; return hook((db, res, rej) => { db.run(query, { ...data, $id: id }, function (error) { if (error) rej(error); res(); }); }); } function getInsert(info) { if (info.isUsing ?? null === null) info.isUsing = 1; info.isUsing = info.isUsing ? 1 : 0; let key = Object.keys(info); let key2 = key.map((k) => `\$${k}`); let data = Object.fromEntries(key2.map((k, i) => [k, info[key[i]]])); return { key, key1: key, key2, data }; } function insertItem(name, info) { let { key1, key2, data } = getInsert({ ...info }); let query = `INSERT INTO ${name} (${key1}) VALUES (${key2})`; return hook((db, res, rej) => { db.run(query, { ...data }, function (error, data) { if (error) rej(error); db.get(`select last_insert_rowid() as id from ${name} LIMIT 1;`, function (error, data) { if (error) rej(error); res(data.id); }); }); }); } function deleteItem(name, id) { let query = `DELETE FROM ${name} WHERE id = $id`; return hook((db, res, rej) => { db.run(query, { $id: id }, function (error, data) { if (error) rej(error); res(data); }); }); } var db = /*#__PURE__*/Object.freeze({ __proto__: null, deleteItem: deleteItem, insertItem: insertItem, tableList: tableList, updateItem: updateItem }); function isIP(str) { return node_net.isIP(str) > 0; } const MATCH = { END: "#", IP: ".", BAN: "!", REG: "/", FUZZY: "*", FUN: "$" }; let domainTree = {}; let domainAction = { ban: () => MATCH.BAN, i192(domain, ban, msg) { let n = Number(domain[0]) || 0; return `192.168.12.${n}`; }, }; function never(msg) { throw new Error(msg); } function toA(matchData, site, t1) { matchData = Array.isArray(matchData) ? matchData : [matchData]; matchData = matchData.flatMap((ip) => { if (!ip) return []; try { return dns.A({ name: site, address: ip || never("[ip] cannot be empty, [dns.A] must have an [address]"), ttl: Date.now() - t1 + 10, }); } catch (e) { console.error(e); return []; } }); return matchData; } function next(target, arr, index) { if (!target) return null; return nodeMatch(target, arr, index + 1); } function nodeMatch(target, arr, index = 0) { if (!target) return null; if (index == arr.length) { let end = target[MATCH.END]; if (end) { if (end[MATCH.BAN]) return MATCH.BAN; if (end[MATCH.IP]?.length > 0) return end[MATCH.IP]; if (end[MATCH.FUN]?.length > 0) return end[MATCH.FUN]; } return null; } let key = arr[index]; let regMatch = (target, arr, index) => { if (!target) return null; let key = target[MATCH.REG] || []; key = key.filter((str) => { str = str.replace(/^\//, "").replace(/\/$/, ""); return new RegExp(str).test(arr[index]); }); return key.reduce((a, b) => a || (b && next(target[b], arr, index)), null); }; return next(target[key], arr, index) || regMatch(target, arr, index) || next(target[MATCH.FUZZY], arr, index); } function localMatch(site) { let t1 = Date.now(); let target = domainTree; let arr = site.split("."); let result = nodeMatch(target, [...arr].reverse(), 0); if (!Array.isArray(result)) { result = [result]; } let shield = false; result = result.flatMap((v) => { if (isIP(v)) return v; if (v === MATCH.BAN) { shield = true; } let fun = domainAction[v]; if (fun) { let r = fun(arr, MATCH.BAN, "// todo path"); if (r === MATCH.BAN) { shield = true; } return r; } return []; }); if (shield) result = []; result = toA(result, site, t1); return { data: result ?? [], shield: shield }; } function creatMap(data, clear = true) { let sm = clear ? {} : domainTree; data.forEach((item) => { let nameList = nameSplit(item.domain); let node = sm; [...nameList.reverse(), MATCH.END].forEach((nameStr) => { if (nameStr.startsWith("/")) { node[MATCH.REG] = [...(node[MATCH.REG] || []), nameStr]; } if (!node[nameStr]) { node[nameStr] = {}; } node = node[nameStr]; }); if (isIP(item.action)) { let v = node[MATCH.IP] || []; node[MATCH.IP] = [...v, item.action]; } else if (item.action == "ban") { node[MATCH.BAN] = true; } else { let v = node[MATCH.FUN] || []; node[MATCH.FUN] = [...v, item.action]; } }); domainTree = sm; } function nameSplit(name) { let arr = name.split("."); let subIndex = arr.map((item, i) => { let s = false, e = false; if (item.startsWith("/")) { s = true; } if (item.endsWith("/")) { e = true; } if (s || e) return { i: i, s, e }; return null; }); let result = []; let startArr = subIndex.filter((item) => !!item && item.s); if (startArr.length == 0) { return arr; } result = arr.slice(0, startArr[0]?.i || 0); let end = null; startArr.forEach((start) => { end = subIndex.find((v, i) => v && v.e == true && i >= start.i); let r = arr.slice(start.i, end.i + 1); if (r.length > 0) { r = r.join("."); result.push(r); } }); result = [...result, ...arr.slice(end?.i + 1)]; return result; } function ask(siteName, address = "8.8.8.8", port = 53, timeout = 1000) { let server = { address: address || "8.8.8.8", port: port || 53, type: "udp" }; return new Promise((resolve, reject) => { var question = dns.Question({ name: siteName, type: "A", }); try { var req = dns.Request({ question: question, server: server, timeout: timeout || 1000, }); req.on("timeout", function () { reject({ type: "timeout", siteName: siteName, msg: "Timeout in making request", server: server, }); }); req.on("message", function (err, answer) { resolve(answer.answer); }); req.on("end", function () { resolve(null); }); req.send(); } catch (e) { reject({ type: "run error", siteName: siteName, server: server, error: e, }); } }); } async function askList(siteName, log = null) { let list = dnsList(); let data = null; let address = null; for (let i = 0; i < list.length; i++) { address = list[i]; data = await ask(siteName, address).catch((e) => { if (!log) console.error(e); if (log) log(e); return null; }); if (data && Array.isArray(data) && data.length > 0) break; } return { data, dns: address }; } let flag = false; function resultLog(title, siteName, result, log = null) { let data = (result || []) .map((item) => { item = JSON.parse(JSON.stringify(item)); if (item.class != null) { item.class = [null, "IN"][item.class] || item.class; item.type = { 1: "A", 2: "NS", 5: "CNAME", 6: "SOA", 12: "PTR", 15: "MX", 16: "TXT", 28: "AAAA", 33: "SRV", 35: "NAPTR", 52: "TLSA", }[item.type] || item.type; } return JSON.stringify(item); }) .join("\n "); let msg = [ [title, "=>", siteName, "", "(count:" + (result?.length || 0) + ")"], [" ", data], ["\n"], ]; msg.forEach((line) => { if (log) { log.push(line); } console.log(...line); }); } function domainLookup(request, response, log = null) { let siteName = request.question[0].name; let replyAnswer = (answer) => { answer = answer || []; answer.forEach((item) => { response.answer.push({ ...item }); }); response.send(); }; let result = localMatch(siteName); let isEmpty = (result.data?.length || 0) == 0; let shield = result.shield; let needLocal = !isEmpty || shield; let needRemote = isEmpty && !shield; if (needLocal) { resultLog("DNS [local]", siteName, result.data, log); replyAnswer(result.data); } if (needRemote) { askList(siteName, (e) => { console.log(e); resultLog("DNS [" + e.server.address + "]", siteName, [e], log); }) .then(({ data, dns }) => { resultLog("DNS [" + dns + "]", siteName, data, log); replyAnswer(data); }) .catch((e) => { console.error(e); console.log("error", e); resultLog("DNS Error [" + e.name + "] " + e.message, siteName, log); response.send(); }); } } function healthCheck(server) { flag = true; const address = server.address(); console.log(`dns server listening ${address.address}:${address.port}`); console.log("成功", `dig home.abc.loc @127.0.0.1 -p ${address.port}`, "\n\n"); } function errorLog(err, buff, req, res) { console.log(err.stack); } function dnsServer(address, port) { flag = false; var server = dns.createServer(); server.once("listening", () => healthCheck(server)); server.on("request", domainLookup); server.on("error", errorLog); server.serve(port, address); setTimeout(() => { if (!flag) { console.log( "DNS启动失败", "\n\t", `1.请检查[${address}:${port}]端口是否被占用;`, "\n\t", "2.端口小于1024时,需要管理员或root权限;" ); } }, 50); return server; } function read(src) { try { return fs.readFileSync(src).toString(); } catch (error) { console.error("read file error", src); return null; } } const httpServe = express(); httpServe.use(express.static("html")); httpServe.use(express.json()); httpServe.use(express.urlencoded({ extended: true })); loadApi(); httpServe.get("**", (req, res, next) => { next(); res.send("--404--"); }); function loadApi(src = "./serve/api") { fs.readdirSync(`${src}`) .filter((v) => { return /\.js$/.test(v); }) .forEach((file) => { let code = read(`./${src}/${file}`); let fun = new Function("arg", `${code}; init(arg);`); fun({ httpServe, db, domainLookup }); console.log(" load", file); }); } function loadDnsData() { tableList("domain", { isUsing: true }).then((list) => { return creatMap(list); }); tableList("dns", { isUsing: true }).then((list) => { dnsList(list.map((v) => v.ip)); }); } function main() { loadDnsData(); setTimeout(() => { dnsServer(address, port); }, 100); setTimeout(() => { if (webPage) { httpServe.listen(httpPort, () => { console.log("DNS Web Management"); console.log(`http://${address}:${httpPort}/`); }); } }, 800); } main();