Files
NeoTachyon/tachyon/files/usr/lib/dns/apply.uc
T

393 lines
13 KiB
Ucode

#!/usr/bin/env ucode
let fs = require("fs");
let uci = require("core.uci");
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const SB_DNS_INBOUND_ADDRESS = getenv("SB_DNS_INBOUND_ADDRESS") || "127.0.0.42";
let common = require("core.common");
let as_string = common.as_string;
let shell_quote = common.shell_quote;
const DNSMASQ_INIT = getenv("DNSMASQ_INIT") || "/etc/init.d/dnsmasq";
const HOSTS_CACHE_FILE = getenv("TACHYON_HOSTS_CACHE_FILE") || "/etc/tachyon/hosts-lists/combined.txt";
function run(command) {
return system(command) == 0;
}
function uci_available() {
return uci.available();
}
function uci_get(path) {
return uci.get(path);
}
function uci_exists(path) {
return uci.exists(path);
}
function uci_delete(path) {
uci.delete(path);
}
function uci_set(path, value) {
uci.set(path, value);
}
function uci_add_list(path, value) {
uci.add_list(path, value);
}
function uci_del_list(path, value) {
return uci.del_list(path, value);
}
function uci_commit(package_name) {
uci.commit(package_name);
}
function words(value) {
value = trim(as_string(value));
return value == "" ? [] : split(value, /[ \t\r\n]+/);
}
function truthy(value) {
value = lc(as_string(value));
return value == "1" || value == "true" || value == "yes" || value == "on";
}
function starts_with(str, prefix) {
str = as_string(str);
prefix = as_string(prefix);
return substr(str, 0, length(prefix)) == prefix;
}
function list_has(values, needle) {
for (let value in words(values))
if (value == needle || (needle != "" && starts_with(value, needle + "#")))
return true;
return false;
}
function log(message, level) {
level = as_string(level || "info");
run("logger -t " + shell_quote("tachyon") + " " + shell_quote("[" + level + "] " + as_string(message)));
}
function restart_dnsmasq() {
return run("[ -x " + shell_quote(DNSMASQ_INIT) + " ] && " + shell_quote(DNSMASQ_INIT) + " restart");
}
// procd regenerates dnsmasq.conf on reload and only bounces the instance when the
// generated config actually changed, so a reload applies our UCI edits without the
// unconditional stop/start (and the dropped in-flight queries) of a full restart.
// Older or non-procd dnsmasq init scripts have no usable reload, so fall back.
function reload_dnsmasq() {
if (run("[ -x " + shell_quote(DNSMASQ_INIT) + " ] && " + shell_quote(DNSMASQ_INIT) + " reload"))
return true;
log("dnsmasq reload failed; falling back to a full restart", "warn");
return restart_dnsmasq();
}
function dnsmasq_legacy_instance_exists() {
return uci_exists("dhcp.tachyon");
}
function dnsmasq_default_servers() {
return uci_get("dhcp.@dnsmasq[0].server");
}
function dnsmasq_default_has_tachyon_dns() {
return list_has(dnsmasq_default_servers(), SB_DNS_INBOUND_ADDRESS);
}
function dnsmasq_has_tachyon_dns() {
return dnsmasq_default_has_tachyon_dns() || dnsmasq_legacy_instance_exists();
}
function dnsmasq_has_tachyon_managed_state() {
return uci_get("dhcp.@dnsmasq[0].tachyon_server") != "" ||
uci_get("dhcp.@dnsmasq[0].tachyon_noresolv") != "" ||
uci_get("dhcp.@dnsmasq[0].tachyon_cachesize") != "" ||
uci_get("dhcp.@dnsmasq[0].tachyon_rebind_protection") != "" ||
uci_get("dhcp.@dnsmasq[0].tachyon_localuse") != "" ||
uci_get("dhcp.@dnsmasq[0].tachyon_notinterface") != "" ||
dnsmasq_legacy_instance_exists();
}
function dnsmasq_management_disabled() {
return truthy(uci_get(CONFIG_NAME + ".settings.dont_touch_dhcp"));
}
function dnsmasq_default_config_is_complete() {
return dnsmasq_default_has_tachyon_dns() &&
uci_get("dhcp.@dnsmasq[0].noresolv") == "1" &&
uci_get("dhcp.@dnsmasq[0].cachesize") == "0" &&
uci_get("dhcp.@dnsmasq[0].rebind_protection") == "0" &&
uci_get("dhcp.@dnsmasq[0].localuse") == "1" &&
!dnsmasq_legacy_instance_exists();
}
function dnsmasq_legacy_interfaces() {
let legacy_dnsmasq_section = "tachyon";
let legacy_interfaces = uci_get("dhcp." + legacy_dnsmasq_section + ".interface");
if (legacy_interfaces == "")
legacy_interfaces = uci_get(CONFIG_NAME + ".settings.source_network_interfaces");
if (legacy_interfaces == "")
legacy_interfaces = "br-lan";
return legacy_interfaces;
}
function backup_dnsmasq_config_option(key, backup_key) {
if (uci_get("dhcp.@dnsmasq[0]." + backup_key) != "")
return;
let value = uci_get("dhcp.@dnsmasq[0]." + key);
if (value != "")
uci_set("dhcp.@dnsmasq[0]." + backup_key, value);
}
function backup_dnsmasq_server_list() {
if (uci_get("dhcp.@dnsmasq[0].tachyon_server") != "")
return;
for (let server in words(dnsmasq_default_servers())) {
if (server != SB_DNS_INBOUND_ADDRESS && !starts_with(server, SB_DNS_INBOUND_ADDRESS + "#"))
uci_add_list("dhcp.@dnsmasq[0].tachyon_server", server);
}
}
function restore_dnsmasq_config_option(key, backup_key, default_value) {
let value = uci_get("dhcp.@dnsmasq[0]." + backup_key);
if (value != "") {
uci_set("dhcp.@dnsmasq[0]." + key, value);
uci_delete("dhcp.@dnsmasq[0]." + backup_key);
}
else if (as_string(default_value) != "") {
uci_set("dhcp.@dnsmasq[0]." + key, default_value);
}
else {
uci_delete("dhcp.@dnsmasq[0]." + key);
}
}
function dnsmasq_cleanup_legacy_instance() {
let legacy_instance_present = dnsmasq_legacy_instance_exists();
let legacy_interfaces = legacy_instance_present ? dnsmasq_legacy_interfaces() : "";
uci_delete("dhcp.tachyon");
let backup_notinterfaces = uci_get("dhcp.@dnsmasq[0].tachyon_notinterface");
if (backup_notinterfaces != "") {
uci_delete("dhcp.@dnsmasq[0].notinterface");
for (let value in words(backup_notinterfaces))
uci_add_list("dhcp.@dnsmasq[0].notinterface", value);
uci_delete("dhcp.@dnsmasq[0].tachyon_notinterface");
return;
}
if (legacy_instance_present) {
for (let value in words(legacy_interfaces))
uci_del_list("dhcp.@dnsmasq[0].notinterface", value);
}
uci_delete("dhcp.@dnsmasq[0].tachyon_notinterface");
}
function dnsmasq_configure_default_instance() {
let default_has_tachyon_dns = dnsmasq_default_has_tachyon_dns();
backup_dnsmasq_server_list();
if (!default_has_tachyon_dns) {
backup_dnsmasq_config_option("noresolv", "tachyon_noresolv");
backup_dnsmasq_config_option("cachesize", "tachyon_cachesize");
backup_dnsmasq_config_option("rebind_protection", "tachyon_rebind_protection");
backup_dnsmasq_config_option("localuse", "tachyon_localuse");
}
backup_dnsmasq_config_option("addn_hosts", "tachyon_addn_hosts");
uci_delete("dhcp.@dnsmasq[0].addn_hosts");
uci_delete("dhcp.@dnsmasq[0].server");
uci_add_list("dhcp.@dnsmasq[0].server", SB_DNS_INBOUND_ADDRESS);
uci_set("dhcp.@dnsmasq[0].noresolv", "1");
uci_set("dhcp.@dnsmasq[0].cachesize", "0");
uci_set("dhcp.@dnsmasq[0].rebind_protection", "0");
uci_set("dhcp.@dnsmasq[0].localuse", "1");
}
function dnsmasq_restore_default_instance() {
let server_list = dnsmasq_default_servers();
let backup_servers = uci_get("dhcp.@dnsmasq[0].tachyon_server");
let managed_global_dns = list_has(server_list, SB_DNS_INBOUND_ADDRESS);
uci_delete("dhcp.@dnsmasq[0].server");
if (backup_servers != "") {
for (let value in words(backup_servers))
uci_add_list("dhcp.@dnsmasq[0].server", value);
uci_delete("dhcp.@dnsmasq[0].tachyon_server");
}
else {
for (let value in words(server_list)) {
if (value != SB_DNS_INBOUND_ADDRESS && !starts_with(value, SB_DNS_INBOUND_ADDRESS + "#"))
uci_add_list("dhcp.@dnsmasq[0].server", value);
}
}
uci_delete("dhcp.@dnsmasq[0].tachyon_server");
let noresolv = uci_get("dhcp.@dnsmasq[0].tachyon_noresolv");
if (noresolv != "")
restore_dnsmasq_config_option("noresolv", "tachyon_noresolv", "");
else if (managed_global_dns)
uci_set("dhcp.@dnsmasq[0].noresolv", "0");
let cachesize = uci_get("dhcp.@dnsmasq[0].tachyon_cachesize");
if (cachesize != "")
restore_dnsmasq_config_option("cachesize", "tachyon_cachesize", "");
else if (managed_global_dns)
uci_set("dhcp.@dnsmasq[0].cachesize", "150");
let rebind_protection = uci_get("dhcp.@dnsmasq[0].tachyon_rebind_protection");
if (rebind_protection != "")
restore_dnsmasq_config_option("rebind_protection", "tachyon_rebind_protection", "");
else if (managed_global_dns)
uci_delete("dhcp.@dnsmasq[0].rebind_protection");
let localuse = uci_get("dhcp.@dnsmasq[0].tachyon_localuse");
if (localuse != "")
restore_dnsmasq_config_option("localuse", "tachyon_localuse", "");
else if (managed_global_dns)
uci_delete("dhcp.@dnsmasq[0].localuse");
restore_dnsmasq_config_option("addn_hosts", "tachyon_addn_hosts", "");
}
// Resolves sing-box multi-fallback PID detection across pidfiles, ubus procd, and pidof
function sing_box_running_pid() {
for (let path in [ "/var/run/sing-box.pid", "/var/run/sing-box/sing-box.pid" ]) {
let pid = trim(fs.readfile(path) || "");
if (pid != "" && run("kill -0 " + shell_quote(pid) + " 2>/dev/null"))
return pid;
}
let p = fs.popen("ubus call service list '{\"name\":\"sing-box\"}' 2>/dev/null", "r");
if (p) {
let out = p.read("all");
p.close();
if (out != null && out != "") {
let matched = match(out, /"pid":\s*([0-9]+)/);
if (matched && matched[1] != "") {
let pid = matched[1];
if (run("kill -0 " + shell_quote(pid) + " 2>/dev/null"))
return pid;
}
}
}
let p2 = fs.popen("pidof sing-box 2>/dev/null", "r");
if (p2) {
let out2 = p2.read("all");
p2.close();
if (out2 != null && out2 != "") {
let fields = split(trim(out2), /[ \t\r\n]+/);
if (length(fields) > 0 && fields[0] != "") {
let pid = fields[0];
if (run("kill -0 " + shell_quote(pid) + " 2>/dev/null"))
return pid;
}
}
}
return "";
}
function dnsmasq_configure(force) {
if (!uci_available())
return true;
if (as_string(force) != "force" && uci_get(CONFIG_NAME + ".settings.shutdown_correctly") == "0") {
if (dnsmasq_default_config_is_complete()) {
log("Previous Tachyon shutdown was unclean; dnsmasq already points to sing-box", "info");
return true;
}
log("Previous Tachyon shutdown was unclean and dnsmasq is not ready; applying Tachyon DNS settings", "info");
}
if (as_string(force) != "force") {
let sb_pid = sing_box_running_pid();
if (sb_pid == "") {
log("sing-box is not running; dnsmasq will forward to system DNS until sing-box starts", "warn");
return true;
}
}
log("Configuring dnsmasq to forward DNS to sing-box", "info");
dnsmasq_cleanup_legacy_instance();
dnsmasq_configure_default_instance();
uci_commit("dhcp");
return reload_dnsmasq();
}
function dnsmasq_restore(force, quiet) {
if (!uci_available())
return true;
if (!quiet)
log("Restoring DNS settings in dnsmasq", "info");
if (as_string(force) != "force" && uci_get(CONFIG_NAME + ".settings.shutdown_correctly") == "1") {
if (!dnsmasq_has_tachyon_dns()) {
log("dnsmasq already uses non-Tachyon DNS settings; restore is not required", "info");
return true;
}
log("Tachyon DNS settings are still present after a clean shutdown; restoring DNS settings in dnsmasq", "info");
}
dnsmasq_cleanup_legacy_instance();
dnsmasq_restore_default_instance();
uci_commit("dhcp");
return reload_dnsmasq();
}
function failsafe_restore() {
if (!uci_available())
return true;
if (dnsmasq_management_disabled()) {
if (!dnsmasq_has_tachyon_managed_state()) {
log("DNS rollback skipped: dont_touch_dhcp is enabled and no Tachyon dnsmasq changes were found", "info");
return true;
}
log("Rolling back previous Tachyon dnsmasq changes because dont_touch_dhcp is enabled", "warn");
}
else {
log("Rolling back Tachyon DNS changes in dnsmasq", "warn");
}
dnsmasq_restore("force", true);
return true;
}
let mode = ARGV[0] || "";
if (mode == "configure")
exit(dnsmasq_configure(ARGV[1]) ? 0 : 1);
else if (mode == "restore")
exit(dnsmasq_restore(ARGV[1]) ? 0 : 1);
else if (mode == "failsafe-restore")
exit(failsafe_restore() ? 0 : 1);
else if (mode == "has-tachyon-dns")
exit(dnsmasq_has_tachyon_dns() ? 0 : 1);
else if (mode == "has-managed-state")
exit(dnsmasq_has_tachyon_managed_state() ? 0 : 1);
else if (mode == "default-config-complete")
exit(dnsmasq_default_config_is_complete() ? 0 : 1);
warn("Usage: dns/apply.uc <configure|restore|failsafe-restore|has-tachyon-dns|has-managed-state|default-config-complete>\n");
exit(1);